Official Python client SDK for the IICP protocol
Project description
iicp-client · Python SDK
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 --upgrade iicp-client
Requires Python ≥ 3.11 and httpx.
Upgrade note (0.7.69) — upgrade provider nodes so normal
iicp-node serveprocesses keep the unattended updater evidence, refuse keyless plaintext by default, and automatically try a Quick Tunnel before relay when a Docker/home-network endpoint is not directly reachable. This improves adoption without weakening IICP-CX.
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)
Use as a local API proxy (OpenAI / Ollama / Anthropic compat)
Run a local gateway that speaks the OpenAI, Ollama, and Anthropic HTTP APIs and routes every request across the IICP mesh — point any tool you already use at it, no code changes.
pip install 'iicp-client[proxy]'
iicp-node proxy # → http://127.0.0.1:9483
export OPENAI_BASE_URL=http://127.0.0.1:9483/v1 # OpenAI SDK / LangChain / Cursor / liteLLM
export OLLAMA_HOST=http://127.0.0.1:9483 # Open WebUI / Continue.dev / aider / Jan
Loopback-only consumer (never registers with the directory). Override the port with
--port / IICP_PROXY_PORT; co-host next to a node with iicp-node serve --with-proxy.
Every response carries Server: iicp-proxy. Full guide: https://iicp.network/docs/proxy
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 |
routing_epsilon |
0.05 |
ε-greedy exploration probability — with this probability a random node is selected instead of the top-ranked one, promoting discovery of new providers; 0.0 disables; override with IICP_ROUTING_EPSILON |
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 | Opens a Quick Tunnel if available → otherwise auto-elects 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). The official image includes cloudflared, so if no public endpoint is
configured the node first tries a zero-account Quick Tunnel, then falls back to relay. For
stable direct hosting, 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 → Quick Tunnel, then relay
When no direct path is possible, the SDK automatically finds a relay:
NAT tier=3: no direct or IPv6 endpoint available.
Opening Quick Tunnel...
No tunnel available; auto-electing relay from directory...
Auto-elected relay: relay.example.com:9485
With cloudflared available, the node registers its own temporary HTTPS tunnel URL.
If that is unavailable, it connects outbound to the elected relay, which forwards inbound
tasks down the relay path. Re-registration happens automatically when either path 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
))
Security note: relay bind authentication is pending (#510) — only run a relay accept port on networks you trust until the signed-bind mechanism ships.
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_TUNNEL=0 # opt out of Quick Tunnel fallback
IICP_EXTERNAL_IP_PROBE_URL=https://api.ipify.org # WAN IP probe (default)
Operator identity
Your operator identity is an ed25519 keypair — its public key is your operator_id (the
directory stores it as operator_pubkey). One identity spans every node you run: it binds them to
you (nodes show Operated by <your name> ✓), earns a
founder ordinal, and rolls each node's credits into one operator
wallet. Your display_name is the public, mutable handle; your contact stays local.
iicp-node init # create your key-backed identity (~/.iicp/operator.json)
iicp-node serve --node mynode # signs an operator→node delegation; binds the node to you
iicp-node operator rename "NewName" # change your public display_name (signed)
iicp-node operator encrypt # password-encrypt the secret at rest ($IICP_OPERATOR_PASSPHRASE)
iicp-node operator decrypt # remove at-rest encryption
The key is the identity — whoever holds ~/.iicp/operator.json controls it (its nodes, ordinal,
and wallet); there is no central recovery. Back it up (encrypted), never commit or share it; lose it
and the identity, with its founder ordinal, is gone.
Full guide: iicp.network/docs/operator-identity
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 the unit suite
ruff check src tests # lint
Links
- Protocol spec — full IICP specification
- Node setup guide — run your own node
- Error reference — all error codes
- iicp-client-typescript — TypeScript SDK
- iicp-client-rust — Rust SDK
Apache 2.0 · iicp.network
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file iicp_client-0.7.69.tar.gz.
File metadata
- Download URL: iicp_client-0.7.69.tar.gz
- Upload date:
- Size: 330.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64344224f5c1b54a6b9419d4f142a7711096c89b4e20c3c485de693a4efb41c0
|
|
| MD5 |
23aa5abf696642979e372132a5931c46
|
|
| BLAKE2b-256 |
7ac2899a62817e4ef970c7cf8e1f4a89f415edb9f35786a6197dcc01e2122e19
|
File details
Details for the file iicp_client-0.7.69-py3-none-any.whl.
File metadata
- Download URL: iicp_client-0.7.69-py3-none-any.whl
- Upload date:
- Size: 246.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7222ecbb52d340d66367fad0df34642ac1fa7901e006118dece003963ebf87fc
|
|
| MD5 |
22c153dcba4c44cdce088b99f91f3013
|
|
| BLAKE2b-256 |
93b99803f56fb00b6656343639d1c64783b9126ec643bb83b2f1db9c91db33fb
|