iicp-client · Rust SDK
Use the open AI mesh from your Rust 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
Or add to Cargo.toml directly:
[]
= "0.7.86"
One-line test
Install the CLI and ask the mesh:
What good looks like:
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.
MCP gateway safety
iicp-node mcp-gateway --tools format_json,summarize_text advertises only the
tools you name. Shell, file, network, browser, credential, system-control and
regulated-decision tools are denied by default. Enabling one requires all four
controls: --allow-dangerous-tools, --authz-policy ID, --sandbox container
and --audit-redaction (equivalent IICP_MCP_* environment variables exist).
Policy receipts include risk/decision metadata and argument counts, never tool
arguments, prompts, credentials or response content.
Use from Rust
use ;
async
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.
use ;
let reply = client.chat.await?;
For stricter deployments, require a minimum policy-manifest identity level before any prompt leaves the client. This keeps the default open mesh behavior unchanged, but lets controllers fail closed on self-attested or rotated/revoked providers.
let reply = client.chat.await?;
Migrate from existing AI tools
Direct call:
// Before: call one vendor endpoint directly.
// After: ask IICP to discover and route by capability.
let reply = client.chat.await?;
Existing OpenAI-compatible tools:
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 sensitivefor fail-closed no-remote behavior,eu-restrictedfor EU/EEA node filtering, orstrict-policywhen 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 crates.io publishes a newer stable release, serve installs it with
cargo install iicp-client --force --features nat,iicp-tcp 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 restart
policy such as --restart unless-stopped so 0.7.80 can intentionally exit from
a confirmed tunnel-dead state and let Docker bring it back cleanly.
Or for the latest unreleased code:
[]
= { = "https://github.com/RobLe3/iicp-client-rust" }
Architecture — consumer or provider?
This SDK covers both sides of the IICP protocol:
| Role | What you do | Type |
|---|---|---|
| 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. For production provider nodes backed by Ollama/vLLM, see iicp.network/docs/node-setup.
Library quickstart
chat() discovers the best node and submits the task internally (SDK-01) — no
manual node selection needed.
use ;
async
Need the discovered nodes directly? Call discover yourself — the third
argument is an optional W3C traceparent for trace propagation:
let nodes = client.discover.await?;
let node = nodes.nodes.into_iter.next.expect;
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.
# OpenAI SDK / LangChain / Cursor / liteLLM
# Open WebUI / Continue.dev / aider / Jan
Loopback-only consumer (never registers with the directory). The proxy is behind the
proxy Cargo feature (kept out of default so library builds stay lean). 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
use ClientConfig;
let config = ClientConfig ;
| 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 |
routing_policy |
RoutingPolicy::default() |
Pre-dispatch remote-routing gate; use Sensitive, EuRestricted, StrictPolicy, or an explicit debug override for special cases |
node_token |
None |
Bearer token for authenticated nodes |
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
use DiscoverOptions;
let nodes = client.discover.await?;
Error handling
use IicpError;
match client.submit.await
Error codes match the IICP error reference.
Serving as a node — handler contract
When you run a serving node (IicpNode::serve), your handler returns the inner result
value; serve() wraps it in the TaskResponse.result envelope for you. Do not return
an already-wrapped {"result": ...} value — that double-nests the response and breaks
cross-flavour interop with the Python/TS SDKs (response shape must be {"result": {...}}).
The backends::invoke_backend / openai_compat::invoke helpers return a
{"result": ...} consumer envelope, so when using them as a serve handler, unwrap the
inner value first:
let v = invoke_backend
.await
.unwrap_or_else;
// serve() re-wraps in TaskResponse.result — return the inner value to stay single-level.
Ok
Backends — pick an inference engine
iicp-node serve (and the backends::invoke_backend dispatch) supports four
backend engines, selected with --backend-type / IICP_BACKEND_TYPE
(default openai_compat):
--backend-type |
Speaks | Typical backend |
|---|---|---|
openai_compat |
OpenAI /v1/* |
Ollama, LM Studio, any OpenAI-compatible server |
vllm |
OpenAI /v1/* |
vLLM OpenAI server (default port 8000) |
llamacpp |
OpenAI /v1/* |
llama.cpp llama-server (default port 8080) |
anthropic |
Anthropic Messages API (POST /v1/messages) |
Anthropic API → first-class Claude |
The anthropic backend translates the IICP llm:chat:v1 task into an Anthropic
Messages request and translates the reply back to the OpenAI chat-completion
shape — so a Claude-backed node looks identical to any other node to IICP
clients. It hoists system-role messages into the top-level system param, sends
x-api-key + anthropic-version headers, and defaults max_tokens (Anthropic
requires it). With no --backend-url override it targets https://api.anthropic.com/v1.
# Serve Claude as an IICP node
# or set IICP_BACKEND_TYPE / IICP_BACKEND_API_KEY in the environment
The API key comes from --backend-api-key (env IICP_BACKEND_API_KEY). For the
OpenAI-compatible backends this is sent as a Bearer token; for anthropic it is
sent as the x-api-key header.
Input modalities — text, image, audio
A node advertises the input modalities each model accepts under
capabilities[].input_modalities, detected from the model name (conservative
name-pattern matching, ADR-046 / #408 / #414):
| Model name contains | Advertised modalities |
|---|---|
vl / vision / llava |
["text", "image"] |
audio / voxtral |
["text", "audio"] |
omni |
["text", "image", "audio"] |
| anything else | ["text"] |
Each modality is a modality of chat, not a separate intent. A single node hosting
several models advertises one capability object per (intent, input_modalities)
group, so a text model and a vision model on the same node surface as distinct
capabilities. Image and audio are passed through OpenAI-style content parts
(text and image_url blocks); the anthropic backend maps image_url parts
(data-URL or remote URL) into native Anthropic image content blocks.
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 binary auto-increments to the next free port when 9484 is already
in use, so several nodes on one host don't need hand-picked ports — first binds
9484, second 9485, third 9486, etc. Each node gets its own port (hence its own NAT
pinhole); multiple models on one node share that single port. Auto-increment is
skipped when you pass an explicit --public-endpoint.
NAT traversal — automatic (v0.7.3+)
Since v0.7.3, NAT detection runs automatically on every iicp-node serve startup — no flags
needed. Requires the nat feature (UPnP detection):
[]
= { = "0.7", = ["nat"] }
# For relay substrate (CGNAT fallback): add "iicp-tcp"
= { = "0.7", = ["nat", "iicp-tcp"] }
| Tier | When | What happens |
|---|---|---|
| 0 | VPS/cloud (public IP on NIC) or IICP_PUBLIC_ENDPOINT set |
Registers directly |
| 1a | Home router with UPnP, no CGNAT | Port-forward via UPnP → register WAN IP |
| 1b | CGNAT + IPv6 + AddPinhole works | Registers IPv6 with firewall rule |
| 1c | CGNAT + IPv6 + AddPinhole fails (FRITZ!Box error 606) | Registers IPv6 + 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
Docker bridge (-p 8020:8020) — UPnP is skipped (reaches Docker NAT, not home router).
The official image includes cloudflared, so without a public endpoint it first tries a
zero-account Quick Tunnel, then 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:
CGNAT + no IPv6 → Quick Tunnel, then relay:
[iicp-node] NAT tier=3: opening Quick Tunnel...
[iicp-node] no tunnel available — auto-electing relay from directory...
[iicp-node] auto-elected relay: relay.example.com:9485
Running a relay-capable node (relay operator)
use ;
let node = new;
Relay workers request short-lived directory-signed bind tickets when they have a saved node
token. Relay operators can enforce them with IICP_RELAY_REQUIRE_BIND_TICKET=1 and the
directory's Ed25519 verification key in IICP_RELAY_BIND_TICKET_PUBLIC_KEY. Keep strict mode
enabled on public relays; unsigned compatibility mode is intended only for staged migration.
Opt-out / override
IICP_AUTO_DETECT_NAT=false # disable detection entirely
IICP_PUBLIC_ENDPOINT=http://x.x.x.x:8020 # trust this endpoint
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_RELAY_WORKER_ENDPOINT=host:9485 # specific relay instead of auto-elect
Publish a signed node policy
Operators can describe public handling rules in a local JSON file and have the client sign it with their existing operator identity before registration:
# or: IICP_POLICY_MANIFEST_FILE=~/.iicp/node-policy.json
The source file stays local. The registration contains the public policy document, its public operator key, timestamps, and detached Ed25519 signature—never the operator secret. The same signed document is reused during recovery re-registration, so policy does not disappear when a tunnel rotates. A signed declaration is tamper-evident operator evidence, not a legal or privacy certification.
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.
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 | ✓ |
| SDK-02 | task_id auto-generated (UUID v4) |
✓ |
| SDK-03 | Intent URN pattern validation (regex) | ✓ |
| SDK-04 | timeout_ms capped at 120 000 ms |
✓ |
| SDK-05 | Retry on transient errors (429 / 502 / 503 / 504) | ✓ |
| SDK-06 | W3C traceparent propagation (shared across discover + submit) |
✓ |
Conformance tier: iicp:sdk:v1 (spec S.14) · Request a badge
Development
Links
- Protocol spec — full IICP specification
- Node setup guide — run your own node
- Error reference — all error codes
- iicp-client-python — Python SDK
- iicp-client-typescript — TypeScript SDK
Apache 2.0 · iicp.network