iicp-client 0.7.2

Official Rust client SDK for the IICP protocol (ADR-016)
Documentation

iicp-client · Rust SDK

CI 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

Add to Cargo.toml

[dependencies]
iicp-client = "0.2"

Or for the latest unreleased code:

[dependencies]
iicp-client = { git = "https://github.com/RobLe3/iicp-client-rust" }

Quickstart

use iicp_client::{ChatMessage, ChatOptions, ClientConfig, IicpClient};

#[tokio::main]
async fn main() -> iicp_client::Result<()> {
    let client = IicpClient::new(ClientConfig::default())?;

    let nodes = client.discover("urn:iicp:intent:llm:chat:v1", None).await?;
    let node  = nodes.nodes.into_iter().next().expect("no nodes available");

    let reply = client.chat(
        &node,
        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(())
}

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),
    }),
).await?;

Error handling

use iicp_client::IicpError;

match client.submit(&node, 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))

NAT traversal — v0.7.0

IICP nodes pick the best available NAT path automatically (ADR-041):

Tier Method Requirement
0 Direct — publicly routable Open port 8020
1 UPnP/IGD port mapping Home router with UPnP
2 IPv6 firewall pinhole IPv6 + UPnP/IGD2
3 Relay-as-last-resort A relay operator in the mesh

Relay-as-last-resort lets a node behind CGNAT stay reachable by binding an outbound channel to a public relay node that forwards inbound tasks down it. Requires the iicp-tcp feature (adds CBOR framing via ciborium).

Running a relay-capable node (relay operator)

[dependencies]
iicp-client = { version = "0.7", features = ["iicp-tcp"] }
use iicp_client::{IicpNode, NodeConfig};

let node = IicpNode::new(NodeConfig {
    node_id          : "relay-eu-01".into(),
    endpoint         : "http://relay.example.com:8020".into(),
    intent           : "urn:iicp:intent:llm:chat:v1".into(),
    relay_capable    : true,   // accept RELAY_BIND on TCP 9485
    relay_accept_port: 9485,
    enable_mesh      : true,   // gossip relay_capable=true to peers
    ..Default::default()
});

Node behind CGNAT (connects outbound to relay)

let node = IicpNode::new(NodeConfig {
    node_id               : "cgnat-worker-001".into(),
    endpoint              : "http://placeholder".into(), // overwritten on bind
    intent                : "urn:iicp:intent:llm:chat:v1".into(),
    relay_worker_endpoint : Some("relay.example.com:9485".into()),
    ..Default::default()
});

When the worker binds it re-registers with the relay's public address (transport_method="turn_relay"), making it discoverable.


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 429 / 503 planned
SDK-06 W3C traceparent propagation planned

Conformance tier: iicp:sdk:v1 (spec S.14) · Request a badge


Development

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

Links


Apache 2.0 · iicp.network