Skip to content

RobLe3/iicp-client-rust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

114 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

iicp-client · Rust SDK

License Protocol crates.io

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

Install

cargo add iicp-client

Or 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-client

Or for the latest unreleased code:

[dependencies]
iicp-client = { git = "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.


Quickstart

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");

Configuration

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

Discover options

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?;

Error handling

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.


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 = 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))

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
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 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):

[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

Environment-specific behaviour

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-node

CGNAT + no IPv6 → automatic relay:

[iicp-node] NAT tier=3: auto-electing relay from directory...
[iicp-node] auto-elected relay: relay.example.com:9485

Running a relay-capable node (relay operator)

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()
});

Opt-out / override

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

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

cargo test          # 157 tests
cargo clippy        # lint
cargo build --release
cargo run --example quickstart

Links


Apache 2.0 · iicp.network

About

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

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors