ic-rig
A lean, modular Rust library for building LLM agent applications — designed to run anywhere, including ICP (Internet Computer Protocol) WASM canisters.
No HTTP client included. You bring your own, which means ic-rig works on native Tokio, ICP canisters, WASM runtimes, or anywhere else you can make an HTTP call.
Table of Contents
- Features
- Installation
- Quick Start
- Bringing Your Own HTTP Client
- Providers
- Building an Agent with Tools
- Embeddings
- Vector Search with LSH
- ICP Canister Example
- API Reference
Features
- Provider-agnostic — OpenAI, Anthropic, Google Gemini, and DeepSeek behind a single
CompletionModeltrait - Agentic tool-call loop — built-in multi-turn reasoning with automatic tool dispatch
- Embeddings + semantic search — batch embeddings with LSH approximate nearest-neighbor indexing and configurable distance metrics
- WASM/ICP-first — no
Send + Syncrequirements on futures, no bundled HTTP dependencies - Minimal footprint — only
serde,serde_json, andthiserrorare required
Installation
Add ic-rig to your Cargo.toml and enable the providers you need:
[]
= { = "0.1", = ["openai"] }
# or
= { = "0.1", = ["anthropic"] }
# or
= { = "0.1", = ["openai", "anthropic", "gemini", "deepseek"] }
Available feature flags:
| Flag | Enables |
|---|---|
openai |
OpenAI completion + embeddings |
anthropic |
Anthropic Claude completion |
gemini |
Google Gemini completion + embeddings |
deepseek |
DeepSeek completion |
Quick Start
use Agent;
use ;
// my_http implements ic_rig::http::HttpClient
let client = new;
let model = client.model;
let agent = builder
.preamble
.max_tokens
.build;
let reply: String = agent.prompt.await?;
println!; // "Paris."
Bringing Your Own HTTP Client
ic-rig ships no HTTP client. You implement the HttpClient trait once for your platform, then pass it to any provider client.
use ;
;
Native (reqwest) example
use ;
;
Providers
OpenAI
use ;
let client = new;
let model = client.model;
let embedder = client.embedding_model.with_dimensions;
Current generation (recommended): GPT_5_6, GPT_5_6_TERRA, GPT_5_6_LUNA, GPT_5_6_CYBER, GPT_5_3_CODEX
Previous generation (still active): GPT_5, GPT_5_MINI, GPT_5_NANO, GPT_4_1, GPT_4_1_MINI, GPT_4_1_NANO, GPT_4O, GPT_4O_MINI, O3, O3_MINI, O4_MINI
Available embedding models: TEXT_EMBEDDING_3_LARGE, TEXT_EMBEDDING_3_SMALL, TEXT_EMBEDDING_ADA_002
Note:
GPT_4_TURBOandGPT_35_TURBOare still exported but scheduled for removal by OpenAI on 2026-10-23 — migrate toGPT_5_6/GPT_5_6_TERRAandGPT_5_6_TERRA/GPT_5_6_LUNArespectively.O1andO1_MINIare exported but#[deprecated]— OpenAI already retired them (2025-07-28 and 2025-10-27); useO3/O4_MINI.
Anthropic
use ;
let client = new;
let model = client.model;
Current generation (recommended): CLAUDE_FABLE_5, CLAUDE_OPUS_5, CLAUDE_SONNET_5, CLAUDE_HAIKU_4_5
Previous generation (still active): CLAUDE_OPUS_4_8, CLAUDE_OPUS_4_7, CLAUDE_OPUS_4_6, CLAUDE_SONNET_4_6, CLAUDE_OPUS_4_5, CLAUDE_SONNET_4_5
Note:
CLAUDE_OPUS_4andCLAUDE_SONNET_4are still exported but#[deprecated]— Anthropic has deprecated them in favor of the 5-series (retirement date TBD).CLAUDE_SONNET_4_5's snapshot date was corrected from-20251101to the real-20250929on 2026-08-27; update any code that hardcoded the old string instead of the constant.Note: Anthropic requires
max_tokens. The default is 1024 if you don't call.max_tokens()on the builder.
Google Gemini
use ;
let client = new;
let model = client.model;
let embedder = client.embedding_model;
Gemini 3 (current generation, recommended): GEMINI_3_1_PRO_PREVIEW, GEMINI_3_7_FLASH, GEMINI_3_6_FLASH, GEMINI_3_5_FLASH, GEMINI_3_5_FLASH_LITE, GEMINI_3_1_FLASH_LITE
Gemini 2.5 (previous generation; GA-stable until 2026-10-16): GEMINI_2_5_PRO, GEMINI_2_5_FLASH, GEMINI_2_5_FLASH_LITE
Available embedding models: GEMINI_EMBEDDING_001, GEMINI_EMBEDDING_2_PREVIEW (multimodal)
Note:
GEMINI_2_0_FLASH,GEMINI_2_0_FLASH_LITE,GEMINI_1_5_PRO,GEMINI_1_5_FLASH,TEXT_EMBEDDING_004, andEMBEDDING_001are still exported but#[deprecated]— Google has already shut all of them down.GEMINI_2_5_PROwas repointed from the dated preview IDgemini-2.5-pro-preview-05-06(which Google now redirects) to the stablegemini-2.5-pro.
DeepSeek
DeepSeek's API is OpenAI-compatible. In thinking mode the model produces a chain-of-thought reasoning_content field; when content is empty ic-rig surfaces the reasoning trace as the reply so the agent loop always gets a usable string.
use ;
let client = new;
let flash = client.model;
let pro = client.model;
Available models: DEEPSEEK_V4_FLASH, DEEPSEEK_V4_PRO, DEEPSEEK_V4_FLASH_VISION_EXP (experimental, multimodal)
Note: DeepSeek does not provide an embeddings API; only completion is supported.
DEEPSEEK_CHATandDEEPSEEK_REASONERare still exported but#[deprecated]— DeepSeek fully retired those model IDs on 2026-07-24. Switch toDEEPSEEK_V4_FLASH(their replacement in both non-thinking and thinking mode).
Building an Agent with Tools
ic-rig has a built-in agentic loop that automatically dispatches tool calls and feeds results back to the model until it returns a final text response.
1. Define a tool
use ;
use ;
use json;
;
2. Register tools and run the agent
let agent = builder
.preamble
.tool
.max_tokens
.max_iterations
.build;
let reply = agent.prompt.await?;
// "The current weather in Tokyo is 18.5°C and sunny."
3. Multi-tool agents
let agent = builder
.preamble
.tool
.tool
.tool
.max_tokens
.build;
4. Injecting context (RAG)
let agent = builder
.preamble
.context
.context
.build;
5. Multi-turn chat
prompt() is single-turn. For a back-and-forth conversation, use chat() and maintain a Vec<Message> history yourself:
use Message;
let agent = builder.preamble.build;
let mut history: = Vecnew;
let r1 = agent.chat.await?;
let r2 = agent.chat.await?;
// r2 => "Your name is Alice."
6. Thinking / reasoning models
agent.prompt() and agent.chat() always return the model's final answer — a chain-of-thought / "thinking" trace, if the provider produces one, never ends up mixed into that string. Some models (DeepSeek's reasoning models, Claude's extended thinking, Gemini's thinking models) think by default; use .thinking(false) if you just want a straight answer, or .thinking(true) to make sure it's on:
let agent = builder
.preamble
.thinking // straight answer, no reasoning trace
.build;
let reply = agent.prompt.await?;
// reply is just "408" — no "<thinking>..." trace mixed in, even on a
// reasoning model that would otherwise produce one.
Leaving .thinking(...) unset keeps the provider's own default behavior. Each provider maps this to its own request parameter (DeepSeek's thinking.type, Anthropic's thinking.type, Gemini's thinkingConfig, OpenAI's reasoning_effort) — see CompletionRequest::thinking for the exact per-provider translation and its caveats (e.g. Gemini 3.1 Pro can't fully disable thinking, and pre-4.6 Claude snapshots don't support this at all).
If you're calling a CompletionModel directly instead of going through Agent, the reasoning trace (when present) is on CompletionResponse::reasoning — a separate field from choice, so you can show or log it without it ever contaminating the answer:
let response = model.complete.await?;
if let Some = &response.reasoning
Embeddings
Embedding a list of strings
use EmbeddingsBuilder;
use ;
let model = new.embedding_model;
let results = new
.document?
.document?
.build
.await?;
for in results
Embedding custom types
Implement the Embed trait to embed multiple fields per document:
use ;
let results = new
.documents? // Vec<Article>
.build
.await?;
// results: Vec<(Article, Vec<Embedding>)>
// Each article gets two Embeddings: one for title, one for body.
Distance metrics
Every Embedding implements VectorDistance, giving you direct access to all metrics:
use VectorDistance;
let sim = a.cosine_similarity; // false = vectors not pre-normalised
let dist = a.euclidean_distance;
let dot = a.dot_product;
When the metric is a runtime value (config, user choice), use DistanceMetric instead:
use DistanceMetric;
let metric = Cosine ;
let score = metric.score;
Available variants:
| Variant | Range | Best-first sort |
|---|---|---|
Cosine { normalized: bool } |
[-1, 1] |
descending |
Angular { normalized: bool } |
[0, 1] |
ascending |
Euclidean |
[0, ∞) |
ascending |
Manhattan |
[0, ∞) |
ascending |
Chebyshev |
[0, ∞) |
ascending |
DotProduct |
(-∞, ∞) |
descending |
Vector Search with LSH
LshIndex provides fast approximate nearest-neighbor search using locality-sensitive hashing. It is designed for use inside ICP canisters where you cannot run an external vector database.
Indexing
use LshIndex;
// new(dimensions, num_hyperplanes, num_tables, seed)
let mut index = new;
index.insert;
index.insert;
index.insert;
Searching
search() runs the LSH lookup, scores the candidates, and returns sorted (id, score) pairs in one call. Pass None for the metric to default to cosine similarity:
use DistanceMetric;
use HashMap;
// store maps each ID to its Embedding
let results: = index.search;
// or pick a metric explicitly
let results = index.search;
// results are already sorted best-first
for in &results
Low-level query
If you need the raw candidate IDs without scoring, use query() directly:
let candidates: = index.query;
// score and sort candidates yourself
Tuning
LSH trades recall for speed. The two parameters control the tradeoff:
| Parameter | Higher value | Lower value |
|---|---|---|
num_hyperplanes |
Fewer candidates, faster scoring | More candidates, slower |
num_tables |
Better recall, more RAM | Less RAM, more misses |
A good starting point for 1536-dimensional OpenAI embeddings: LshIndex::new(1536, 12, 6, 42).
ICP Canister Example
use update;
use Agent;
use ;
async
API Reference
Agent<M>
| Method | Description |
|---|---|
Agent::builder(model) |
Create an AgentBuilder |
.prompt(text) |
Single-turn prompt, returns String |
.chat(text, history) |
Multi-turn prompt, updates history in place |
AgentBuilder<M>
| Method | Description |
|---|---|
.preamble(s) |
Set the system prompt |
.tool(t) |
Register a tool |
.temperature(f) |
Sampling temperature |
.max_tokens(n) |
Maximum output tokens |
.thinking(bool) |
Explicitly turn thinking/reasoning mode on or off (default: provider's own default) |
.max_iterations(n) |
Maximum tool-call rounds (default: 10) |
.context(s) |
Inject a RAG chunk into the conversation |
.build() |
Produce Agent<M> |
EmbeddingsBuilder<M, T>
| Method | Description |
|---|---|
EmbeddingsBuilder::new(model) |
Create a builder |
.document(d) |
Add a single document |
.documents(ds) |
Add many documents |
.build() |
Embed all documents, returns Vec<(T, Vec<Embedding>)> |
DistanceMetric
| Method | Description |
|---|---|
.score(a, b) |
Score two embeddings using this metric |
.higher_is_better() |
true for similarity metrics, false for distance metrics |
LshIndex
| Method | Description |
|---|---|
LshIndex::new(dim, hyperplanes, tables, seed) |
Create an index |
.insert(id, vec) |
Index a vector under a string ID |
.search(query, store, metric) |
LSH lookup + score + sort; metric is Option<DistanceMetric> |
.query(vec) |
Raw LSH lookup — returns unscored candidate IDs |
.len() / .is_empty() |
Size queries |
.clear() |
Remove all entries |
License
MIT