Skip to main content

Use the open IICP AI mesh from Python without running a node

Project description

iicp-client · Python SDK

CI License Protocol PyPI

Use the open AI mesh from your Python app. Install the client, send an intent, and get a routed response from an IICP node.

You do not need to run a node to try the client path. Consume first, provide later.

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

Install

pip install --upgrade iicp-client

Requires Python ≥ 3.11 and httpx.

One-line test

iicp-node query "Hello, mesh."

What good looks like:

iicp-node --help       # shows query, serve, proxy, mcp-gateway, credits, ...
which iicp-node        # points to your Python environment
iicp-node --version    # prints iicp-node 0.7.80 or newer

The query command contacts the public directory, discovers a matching live node, routes your prompt, and prints the response. No account, API key, or local node is required for this consumer path.

Privacy note: the selected remote node can read the prompt it executes. IICP-CX keeps key-ready transport/relay paths confidential, but it is not executor-blind inference. For sensitive data, use local/browser inference or a fail-closed routing profile.

Use from Python

from iicp_client import IicpClient, ChatMessage
import asyncio

async def main():
    reply = await IicpClient().chat_async([
        ChatMessage(role="user", content="Hello, mesh.")
    ])
    print(reply.choices[0].message.content)

asyncio.run(main())

Do I need to run a node?

No. Running a node is only needed when you want to provide compute or tools to the mesh. Start as a client; run a node later when you want to contribute.

Routing policy profiles

The client applies routing policy after prompt-free discovery and before the prompt is sent. Defaults stay adoption-friendly but keyless plaintext is still refused.

iicp-node query "Hello" --routing-profile standard        # default encrypted mesh
iicp-node query "Secret" --routing-profile sensitive      # fail closed: no remote executor
iicp-node query "Hello" --routing-profile eu-restricted   # EU/EEA regions only
iicp-node query "Hello" --routing-profile strict-policy   # requires no-retention manifest
from iicp_client import ChatOptions, RoutingPolicy

reply = await IicpClient().chat_async(
    [ChatMessage(role="user", content="Hello")],
    ChatOptions(routing_policy=RoutingPolicy(profile="eu_restricted")),
)

Migrate from existing AI tools

Direct call:

# Before: call one vendor endpoint directly.
# After: ask IICP to discover and route by capability.
reply = await IicpClient().chat_async([
    ChatMessage(role="user", content="Summarize this document.")
])

Existing OpenAI-compatible tools:

pip install 'iicp-client[proxy]'
iicp-node proxy
export OPENAI_BASE_URL=http://127.0.0.1:9483/v1

Then point LangChain, Cursor, liteLLM or another OpenAI-compatible tool at that base URL. Full guide: https://iicp.network/docs/proxy

Provider upgrade note

Upgrade note (0.7.80) — clients now support remote-routing policy profiles that can refuse unsafe remote dispatch before any prompt leaves the caller. Use --routing-profile sensitive for fail-closed no-remote behavior, eu-restricted for EU/EEA node filtering, or strict-policy when a no-retention node policy manifest is required.

Existing provider reachability fixes from 0.7.79 remain intact.

Keeping provider nodes current

Provider nodes run an hourly official-registry check by default (IICP_AUTO_UPDATE=1, IICP_AUTO_UPDATE_INTERVAL_S=3600; minimum 300s). When PyPI publishes a newer stable release, serve runs python -m pip install --upgrade iicp-client and re-execs the node so identity and cached node tokens are preserved.

If a node is older than 0.7.67, perform one manual upgrade/restart first, especially for Dockerized Python or TypeScript providers: early updater wiring did not reliably cover every normal serve path. For Docker, use a Compose restart: unless-stopped policy (or docker run --restart unless-stopped) so 0.7.80 can intentionally exit from a confirmed tunnel-dead state and let Docker bring it back cleanly.


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.


Library 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
routing_policy RoutingPolicy(profile="standard") Pre-dispatch remote-routing gate; use sensitive, eu_restricted, strict_policy, or an explicit debug override for special cases

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. The image also sets IICP_SUPERVISED=1, so with Docker restart policy enabled a confirmed tunnel-dead state exits visibly and lets Docker restart the node. For stable direct hosting, set IICP_PUBLIC_ENDPOINT so the node knows its real address:

# docker-compose.yml
restart: unless-stopped
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_TUNNEL_CREATE_MIN_INTERVAL_S=120  # host-wide Quick Tunnel create pacing
IICP_TUNNEL_DEAD_POLICY=auto  # auto|retry|exit|log-only (auto = supervised exit, manual retry)
IICP_SUPERVISED=1             # set by generated services/Docker so supervisors can restart
IICP_AUTO_UPDATE=1            # hourly provider self-update; set 0 to disable
IICP_AUTO_UPDATE_INTERVAL_S=3600  # update cadence in seconds; minimum 300
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


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

iicp_client-0.7.80.tar.gz (350.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

iicp_client-0.7.80-py3-none-any.whl (262.3 kB view details)

Uploaded Python 3

File details

Details for the file iicp_client-0.7.80.tar.gz.

File metadata

  • Download URL: iicp_client-0.7.80.tar.gz
  • Upload date:
  • Size: 350.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for iicp_client-0.7.80.tar.gz
Algorithm Hash digest
SHA256 d53619cbca3f187da0a83c029710b9077b9fad1b4c6423e0a6564e5b29662142
MD5 ef82138785bca6bfafe84aaa683aab3b
BLAKE2b-256 5754f5a1f7aa6dc6da7200fda1a7bd29c5a3a7d196b414076407741b69426cbc

See more details on using hashes here.

File details

Details for the file iicp_client-0.7.80-py3-none-any.whl.

File metadata

  • Download URL: iicp_client-0.7.80-py3-none-any.whl
  • Upload date:
  • Size: 262.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for iicp_client-0.7.80-py3-none-any.whl
Algorithm Hash digest
SHA256 7dea441015297a5afc0d6d7110fa36de54cc37b0f864538e04eb46bfa579ebb9
MD5 04ea84014f0768fae769f2e6d2b2e885
BLAKE2b-256 f19297525d5dc8e4709957e1797968bf29d9d40212eda611f3033142a94f36a5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page