Skip to content

RobLe3/iicp-client-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

122 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

iicp-client · Python SDK

CI License Protocol PyPI

Official Python client library for the IICP protocol — route AI agent tasks by intent across a self-organising mesh of provider nodes. No central broker. No hardcoded endpoints.

urn:iicp:intent:llm:chat:v1  →  discover  →  select  →  submit

Install

pip install iicp-client

Requires Python ≥ 3.11 and httpx.


Architecture — consumer or provider?

This SDK covers both sides of the IICP protocol:

Role What you do Class
Consumer Send AI tasks to the mesh; discover and submit IicpClient
Provider Run a node, register with the directory, serve tasks IicpNode

Consumer and provider can run in the same process. A node that serves requests can also route tasks it can't handle to other mesh nodes (IicpClient inside the task handler).

For production provider nodes backed by Ollama/vLLM, the iicp-node binary (Rust) and the Python adapter (pip install iicp-adapter) provide additional resilience and monitoring. See iicp.network/docs/node-setup.


Quickstart

import asyncio
from iicp_client import IicpClient, ChatMessage

async def main():
    client = IicpClient()

    # chat_async discovers, selects best node, and submits in one call
    response = await client.chat_async(
        messages=[ChatMessage(role="user", content="Hello from IICP!")],
    )
    print(response.choices[0].message.content)

asyncio.run(main())

Synchronous wrapper for scripts and notebooks:

from iicp_client import IicpClient, ChatMessage

client   = IicpClient()
response = client.chat([ChatMessage(role="user", content="Hello from IICP!")])
print(response.choices[0].message.content)

Configuration

from iicp_client import ClientConfig

config = ClientConfig(
    directory_url = "https://iicp.network/api",  # IICP directory
    timeout_ms    = 30_000,                      # max 120 000 (SDK-04)
    region        = "eu-central",                # prefer nodes in region
)
Field Default Description
directory_url "https://iicp.network/api" IICP directory endpoint
timeout_ms 30000 Request timeout — max 120 000 ms
region None Preferred node region
max_retries 3 Retry count for transient errors

Discover options

from iicp_client import DiscoverOptions

node_list = await client.discover_async(
    "urn:iicp:intent:llm:chat:v1",
    DiscoverOptions(
        region         = "eu-central",
        model          = "phi3:mini",
        min_reputation = 0.7,
        limit          = 5,
    )
)
nodes = node_list.nodes  # list of Node objects

Error handling

from iicp_client import IicpClient, IicpError, ChatMessage

client = IicpClient()
try:
    response = client.chat([ChatMessage(role="user", content="hi")])
except IicpError as e:
    print(f"[{e.code}] {e.message}  (HTTP {e.http_status})")

Error codes match the IICP error reference — e.g. task_timeout, capacity_exceeded, no_nodes_available.


Serving as a provider node

import asyncio
from iicp_client import IicpNode, NodeConfig

async def my_handler(task):
    return {"choices": [{"message": {"role": "assistant", "content": "Hello!"}}]}

async def main():
    node = IicpNode(NodeConfig(
        node_id="my-node-001",
        endpoint="http://my.public.host:8020",
        intent="urn:iicp:intent:llm:chat:v1",
        model="llama3:8b",
    ))
    token = await node.register()
    stop = node.serve(my_handler, port=8020, node_token=token)
    try:
        await asyncio.Event().wait()  # run until stopped
    finally:
        stop()

asyncio.run(main())

Listen port — default 9484, auto-increment (v0.7.5+)

The official IICP port 9484 is the default listen port (IICP_PORT, --port). The iicp-node CLI auto-increments to the next free port when 9484 is already in use, so you can run several nodes on one host without picking ports by hand — the first binds 9484, the second 9485, the third 9486, and so on. Each node gets its own port, hence its own NAT pinhole; multiple models served by one node share that single port. Auto-increment is skipped when you pass an explicit --public-endpoint (you own the port mapping in that case). IicpNode.serve(port=…) uses the port you give it as-is (no auto-increment at the library level).


Backends

A provider node forwards each task to an inference backend. The backend is selected with --backend-type (env IICP_BACKEND_TYPE, default openai_compat):

--backend-type Engine Default backend URL API
openai_compat Ollama, LM Studio, any OpenAI-compatible server http://localhost:11434 OpenAI /v1/*
vllm vLLM OpenAI server http://localhost:8000 OpenAI /v1/*
llamacpp llama.cpp llama-server http://localhost:8080 OpenAI /v1/*
anthropic Native Anthropic Messages API — first-class Claude https://api.anthropic.com Anthropic /v1/messages

The anthropic backend speaks the Anthropic Messages API directly (not the OpenAI-compat shim): it translates an IICP llm:chat:v1 task into a Messages request and translates the response back to the OpenAI chat-completion shape, so a Claude-backed node looks identical to an Ollama/vLLM node to any IICP client. Run one with:

iicp-node serve \
  --backend-type anthropic \
  --backend-api-key "$ANTHROPIC_API_KEY" \
  --model claude-opus-4-8

--backend-type anthropic defaults --backend-url to https://api.anthropic.com, so you only pass the key and the model. The key is sent as the x-api-key header; an anthropic-version header (2023-06-01) is added automatically. The Anthropic backend serves urn:iicp:intent:llm:chat:v1 only (the Messages API has no completion/embedding endpoint).

Common serve flags (all also read from env):

Flag Env Default Purpose
--backend-type IICP_BACKEND_TYPE openai_compat Inference engine (table above)
--backend-url IICP_BACKEND_URL http://localhost:11434 Backend base URL
--backend-api-key IICP_BACKEND_API_KEY (empty) Bearer / x-api-key for an auth'd backend
--model IICP_BACKEND_MODEL (auto-detect) Backend model id (e.g. qwen2.5:0.5b, claude-opus-4-8)

The SDK is configured entirely through CLI flags and environment variables — there is no config file.

Input modalities — text, image, audio

A node advertises the input modalities each model accepts in its capabilities, so clients can discover a vision- or audio-capable node. The modality set is auto-detected from the model name:

Model name contains Advertised input_modalities
vl, vision, llava text, image
audio, voxtral text, audio
omni text, image, audio
(anything else) text

These are modalities of the llm:chat:v1 intent, not separate intents. The directory supports a ?modality=image|audio filter on discover so a client can find nodes that accept a given input type.


NAT traversal — automatic (v0.7.3+)

Since v0.7.3, NAT detection runs automatically on every node startup — no flags needed. The SDK tries each path in order and picks the best one for your network:

Tier When What happens
0 VPS/cloud (public IP on NIC) or IICP_PUBLIC_ENDPOINT set Registers directly with that IP
1a Home router with UPnP, no CGNAT Opens a port-forward via UPnP → registers WAN IP
1b CGNAT + IPv6 available + AddPinhole works Registers IPv6 address with firewall rule
1c CGNAT + IPv6 + AddPinhole fails (e.g. FRITZ!Box error 606) Registers IPv6 GUA anyway + logs guidance
3 CGNAT + no usable IPv6 Auto-elects relay from directory → registers via relay
4 Nothing worked Serves locally with operator guidance

Environment-specific behaviour

VPS / bare metal — no action needed. The SDK detects the public IP on the NIC (Tier 0).

Home router (no CGNAT) — UPnP opens a port-forward automatically. One pinhole per port, so three nodes on ports 8020 / 8024 / 8025 open three pinholes.

CGNAT (carrier-grade NAT, e.g. NetCologne DSLite) — IPv4 path is blocked by the ISP. The SDK tries IPv6 instead. If your FRITZ!Box rejects AddPinhole with error 606, the SDK still advertises your IPv6 address (many clients can reach it via stateful firewall) and logs:

WARNING: NAT: IPv6 endpoint http://[2a0a:...]:8020 advertised but firewall pinhole
could not be opened. Open manually: FRITZ!Box → Network → Firewall → IPv6.
Alternatively use IICP_RELAY_WORKER_ENDPOINT for relay-as-last-resort fallback.

Docker bridge (-p 8020:8020) — UPnP is skipped (it would reach the Docker NAT, not your home router). Set IICP_PUBLIC_ENDPOINT so the node knows its real address:

# docker-compose.yml
environment:
  IICP_PUBLIC_ENDPOINT: "http://your-host-ip:8020"
  IICP_BACKEND_URL: "http://host.docker.internal:11434"

Or run with --network host to let UPnP work as on bare metal.

Kubernetes — set IICP_PUBLIC_ENDPOINT to the Service IP or external LoadBalancer:

env:
  - name: IICP_PUBLIC_ENDPOINT
    value: "http://$(LOAD_BALANCER_IP):8020"

CGNAT + no IPv6 → automatic relay

When no direct path is possible, the SDK automatically finds a relay:

NAT tier=3: no direct or IPv6 endpoint available.
Auto-electing relay from directory...
Auto-elected relay: relay.example.com:9485

The node connects outbound to the elected relay, which forwards inbound tasks down the tunnel. Re-registration happens automatically when the relay bind succeeds.

To use a specific relay instead of auto-electing:

IICP_RELAY_WORKER_ENDPOINT=relay.example.com:9485 python -m iicp_client.cli serve ...

Running a relay-capable node (relay operators)

node = IicpNode(NodeConfig(
    endpoint="http://relay.example.com:8020",
    intent="urn:iicp:intent:llm:chat:v1",
    relay_capable=True,      # accept RELAY_BIND on TCP port 9485
    relay_accept_port=9485,
    enable_mesh=True,        # advertise relay_capable=True in gossip
))

Opt-out / override

IICP_AUTO_DETECT_NAT=false   # disable NAT detection entirely
IICP_PUBLIC_ENDPOINT=http://x.x.x.x:8020   # trust this endpoint, skip detection
IICP_EXTERNAL_IP_PROBE_URL=https://api.ipify.org  # WAN IP probe (default)

SDK conformance

Rule Description Status
SDK-01 discover → select → submit pipeline with node retry
SDK-02 task_id auto-generated (UUID v4)
SDK-03 Intent URN pattern validation
SDK-04 timeout_ms capped at 120 000 ms
SDK-05 Retry on 429 / 503 with exponential back-off
SDK-06 W3C traceparent propagation

Conformance tier: iicp:sdk:v1 (spec S.14) · Request a badge


Development

pip install -e ".[dev]"   # install with dev deps
pytest tests/ -v          # run 255 unit tests
ruff check src tests       # lint

Links


Apache 2.0 · iicp.network

About

Python client SDK for IICP — route tasks across an AI agent mesh by intent

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors