ds-api
Your Rust functions. Any LLM. Zero glue code.
cargo add ds-api
The Problem
Building an LLM agent means writing a pile of code that has nothing to do with your actual problem:
- Hand-craft JSON schemas for every tool
- Parse and validate tool arguments from raw JSON
- Detect tool calls in the response
- Implement an agent loop that re-sends results to the model
- Wire up streaming yourself
Every project. Every time.
The Solution
One macro. Your methods become AI tools.
use ;
use StreamExt;
use ;
;
async
No schema. No argument parsing. No loop. Just your function.
Key Features
#[tool] — Zero-boilerplate tool registration
Annotate any async fn. The macro reads your doc comments, infers the JSON schema from your types, and registers everything automatically.
- Doc comment → tool description. No separate description field.
param: descriptionin doc → parameter description. Inline.Option<T>→ optional parameter. The schema marks it non-required automatically.- Return any
impl Serialize.Value, structs, enums,Vec<T>— anything serde can serialize. - Compile error on unsupported parameter types. You find out at build time, not runtime.
Supported parameter types: String, bool, f32/f64, all integer primitives, Vec<T>, Option<T>.
Typed event stream — AgentEvent
chat() returns a stream of strongly-typed events. The compiler forces you to handle every case.
match event?
No if result.is_null() hacks. No optional fields you have to remember to check. Each variant carries exactly what it means.
In streaming mode, Token arrives as SSE deltas. In non-streaming mode, it arrives as one chunk. Your match arm handles both.
Automatic agent loop
The model requests a tool → ds_api executes it → feeds the result back → asks the model again. This continues until the model stops calling tools. You never write that loop.
User prompt
└─▶ API call
└─▶ ToolCall event (model wants data)
└─▶ your function runs
└─▶ ToolResult event (result fed back)
└─▶ API call (model continues)
└─▶ Token events (final answer)
Context window management — automatic summarization
Long conversations are compressed automatically. The default summarizer (LlmSummarizer) calls the model to write a concise semantic summary of older turns, replaces them with a single system message, and keeps the most recent turns verbatim. Your with_system_prompt messages are never touched.
// Default: trigger at ~60 000 estimated tokens, retain last 10 turns.
let agent = new;
// Custom thresholds:
use ;
let agent = new
.with_summarizer;
If you prefer zero extra API calls, use SlidingWindowSummarizer instead — it keeps the last N turns and silently drops everything older:
use SlidingWindowSummarizer;
let agent = new
.with_summarizer;
Your agent stays within context limits without you counting tokens.
Reusable agents — into_agent()
chat() consumes the agent to keep the borrow checker happy inside the async state machine. Get it back when the stream ends:
let mut agent = new
.with_streaming
.add_tool;
loop
Full REPL with persistent conversation history. No cloning. No Arc<Mutex<>>.
OpenAI-compatible providers
DeepseekAgent::custom(token, base_url, model) points the agent at any OpenAI-compatible endpoint. The default LlmSummarizer is automatically configured to use the same provider and model — no extra setup needed.
use ;
use StreamExt;
async
Real Example — Shell Agent
use ;
use StreamExt;
use ;
use Command;
;
async
The model decides when to call the shell. You just receive the events.
What You Never Write
| Without ds_api | With ds_api |
|---|---|
| JSON schema per tool | #[tool] |
| Argument deserialization | automatic |
| Tool call detection | automatic |
| Agent loop | automatic |
| Token counting / context trimming | automatic |
| Streaming SSE wiring | automatic |
Installation
[]
= "0.5"
= { = "1", = ["full"] }
= "0.3"
export DEEPSEEK_API_KEY=your_key_here
Roadmap
OpenAI-compatible providers- Structured output support
#[tool]parameter types: customserdestructs (return types already support anyimpl Serialize)- More examples
License
MIT OR Apache-2.0