Official Rust 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
cargo add iicp-clientOr add to Cargo.toml directly:
[dependencies]
iicp-client = "0.7.35"To run a provider node from the command line, install the iicp-node binary:
cargo install iicp-clientOr for the latest unreleased code:
[dependencies]
iicp-client = { git = "https://github.com/RobLe3/iicp-client-rust" }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.
chat() discovers the best node and submits the task internally (SDK-01) — no
manual node selection needed.
use iicp_client::{ChatMessage, ChatOptions, ClientConfig, IicpClient};
#[tokio::main]
async fn main() -> iicp_client::Result<()> {
let client = IicpClient::new(ClientConfig::default())?;
let reply = client.chat(
vec![
ChatMessage { role: "system".into(), content: "You are a helpful assistant.".into() },
ChatMessage { role: "user".into(), content: "What is IICP?".into() },
],
Some(ChatOptions { timeout_ms: Some(30_000), ..Default::default() }),
).await?;
println!("{}", reply.choices[0].message.content);
Ok(())
}Need the discovered nodes directly? Call discover yourself — the third
argument is an optional W3C traceparent for trace propagation:
let nodes = client.discover("urn:iicp:intent:llm:chat:v1", None, None).await?;
let node = nodes.nodes.into_iter().next().expect("no nodes available");use iicp_client::ClientConfig;
let config = ClientConfig {
directory_url : "https://iicp.network/api".into(), // IICP directory
timeout_ms : 30_000, // max 120 000 (SDK-04)
region : Some("eu-central".into()), // prefer nodes in region
node_token : None, // optional auth token
};| 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 |
node_token |
None |
Bearer token for authenticated nodes |
use iicp_client::DiscoverOptions;
let nodes = client.discover(
"urn:iicp:intent:llm:chat:v1",
Some(DiscoverOptions {
region : Some("eu-central".into()),
model : Some("phi3:mini".into()),
min_reputation: Some(0.7),
limit : Some(5),
}),
None, // optional W3C traceparent
).await?;use iicp_client::IicpError;
match client.submit(request).await {
Ok(resp) => println!("{:?}", resp),
Err(IicpError::Protocol { code, message, status }) =>
eprintln!("[{code}] {message} (HTTP {status})"),
Err(e) => eprintln!("Error: {e}"),
}Error codes match the IICP error reference.
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 = iicp_client::backends::invoke_backend("openai_compat", &opts, &req.intent, &req.payload)
.await
.unwrap_or_else(|e| serde_json::json!({"error_code": 500, "error_message": e}));
// serve() re-wraps in TaskResponse.result — return the inner value to stay single-level.
Ok(v.get("result").cloned().unwrap_or(v))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
iicp-node serve \
--backend-type anthropic \
--model claude-opus-4-8 \
--backend-api-key "$ANTHROPIC_API_KEY"
# or set IICP_BACKEND_TYPE / IICP_BACKEND_API_KEY in the environmentThe 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.
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.
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.
Since v0.7.3, NAT detection runs automatically on every iicp-node serve startup — no flags
needed. Requires the nat feature (UPnP detection):
[dependencies]
iicp-client = { version = "0.7", features = ["nat"] }
# For relay substrate (CGNAT fallback): add "iicp-tcp"
iicp-client = { version = "0.7", features = ["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 | Auto-elects relay from directory |
| 4 | Nothing worked | Serves locally with operator guidance |
Docker bridge (-p 8020:8020) — UPnP is skipped (reaches Docker NAT, not home router).
Set IICP_PUBLIC_ENDPOINT:
docker run -e IICP_PUBLIC_ENDPOINT=http://your-host:8020 \
-e IICP_BACKEND_URL=http://host.docker.internal:11434 \
-p 8020:8020 my-iicp-nodeCGNAT + no IPv6 → automatic relay:
[iicp-node] NAT tier=3: auto-electing relay from directory...
[iicp-node] auto-elected relay: relay.example.com:9485
use iicp_client::{IicpNode, NodeConfig};
let node = IicpNode::new(NodeConfig {
endpoint : "http://relay.example.com:8020".into(),
intent : "urn:iicp:intent:llm:chat:v1".into(),
relay_capable : true, // accept RELAY_BIND on TCP 9485 (requires iicp-tcp)
relay_accept_port: 9485,
enable_mesh : true, // advertise relay_capable=true in gossip
..Default::default()
});IICP_AUTO_DETECT_NAT=false # disable detection entirely
IICP_PUBLIC_ENDPOINT=http://x.x.x.x:8020 # trust this endpoint
IICP_RELAY_WORKER_ENDPOINT=host:9485 # specific relay instead of auto-elect| 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
cargo test # 157 tests
cargo clippy # lint
cargo build --release
cargo run --example quickstart- 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