ds-api-macros 0.1.0

Procedural macros for ds-api (provides the `tool` macro and related helpers)
Documentation

ds-api-workspace/ds-api/README.md

ds-api — Rust client for DeepSeek (refactored)

This crate provides a layered, ergonomic Rust client for the DeepSeek chat API.

Quick Start

  • Requirements: Rust toolchain and an environment variable DEEPSEEK_API_KEY with your API token.
  • The crate exposes both low-level raw types and higher-level ergonomics via ApiRequest, ApiClient, DeepseekConversation, and DeepseekAgent.

Example: simple non-streaming request (async, requires Tokio)

use ds_api::{ApiClient, ApiRequest};
use ds_api::raw::request::message::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = std::env::var("DEEPSEEK_API_KEY")?;
    let client = ApiClient::new(token);

    let req = ApiRequest::deepseek_chat(vec![
        Message::new(ds_api::raw::request::message::Role::User, "Hello from Rust"),
    ])
    .max_tokens(150)
    .json();

    let resp = client.send(req).await?;
    // Print debug representation of the response; adapt to your needs.
    println!("Response: {:?}", resp);
    Ok(())
}

Example: minimal DeepseekAgent with a sample tool

use ds_api::{DeepseekAgent, tool};
use futures::StreamExt;
use serde_json::json;

struct EchoTool;

#[tool]
impl ds_api::Tool for EchoTool {
    async fn echo(&self, input: String) -> serde_json::Value {
        json!({ "echo": input })
    }
}

#[tokio::main]
async fn main() {
    let token = std::env::var("DEEPSEEK_API_KEY").unwrap();
    let agent = DeepseekAgent::new(token).add_tool(EchoTool);

    let mut s = agent.chat("Please echo: hello");
    while let Some(ev) = s.next().await {
        if let Some(content) = &ev.content {
            println!("Assistant: {}", content);
        }
        for tc in &ev.tool_calls {
            println!("Tool call: {} -> {}", tc.name, tc.result);
        }
    }
}

High-level design

  • raw — low-level types that mirror the API JSON (kept under ds_api::raw but not recommended).
  • api — safe, chainable request builder and HTTP client (ApiRequest, ApiClient).
  • conversation — session management and summarization (DeepseekConversation, Summarizer).
  • agent — agent orchestration with tools and auto-summary (DeepseekAgent).

This README documents the new API, breaking changes, migration steps, and examples.

Quick highlights (new API)

  • ApiRequest — chainable, safe builder. Use ApiRequest::builder() or ApiRequest::deepseek_chat(...) / ApiRequest::deepseek_reasoner(...) to choose a model (Model enum is intentionally not exported).
  • ApiClient — owns token/base_url/reqwest::Client; send blocking or streaming requests.
  • DeepseekConversation — manages history and auto-summary via the Summarizer trait. Default summarizer is TokenBasedSummarizer.
  • DeepseekAgent — high-level agent that combines a conversation, tools (via tool macro), and auto summary. Agent yields two-step events for tool calls: first a preview (content + tool call requests) then tool results.
  • raw module still available under ds_api::raw for advanced users, but not part of the primary recommended API.

Breaking changes (important)

This refactor intentionally removes compatibility with older API shapes. Notable breaking items:

  • Request and DeepseekClient (previous high-level types) have been removed. Use ApiRequest and ApiClient instead.
  • NormalChatter and SimpleChatter were removed. Use DeepseekConversation and DeepseekAgent.
  • Model enum is no longer exported directly as part of the public API. Use ApiRequest::deepseek_chat(...) or ApiRequest::deepseek_reasoner(...) to choose a model.
  • Unsafe raw accessors (e.g. from_raw_unchecked, get_raw_mut) have been removed from the public API.
  • Summarization is now pluggable via the Summarizer trait. Default TokenBasedSummarizer skips system messages when estimating tokens and triggers at the default threshold (100_000 tokens estimate).

If you're migrating from the old crate:

  • Replace Request -> ApiRequest
  • Replace DeepseekClient -> ApiClient
  • Replace NormalChatter -> DeepseekConversation (or DeepseekAgent if you need tools)
  • Replace SimpleChatter -> DeepseekConversation thin wrapper as needed

Example: simple non-streaming request

// Build an ApiRequest and send a non-streaming call.
use ds_api::{ApiClient, ApiRequest};
use ds_api::raw::request::message::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = std::env::var("DEEPSEEK_API_KEY")?;
    let client = ApiClient::new(token);

    let req = ApiRequest::deepseek_chat(vec![
        Message::new(ds_api::raw::request::message::Role::User, "Hello from Rust"),
    ])
    .max_tokens(150)
    .json();

    let resp = client.send(req).await?;
    // Print debug representation of the response; adapt to your needs.
    println!("Response: {:?}", resp);
    Ok(())
}

Example: streaming text from ApiClient

use ds_api::{ApiClient, ApiRequest};
use futures::StreamExt;

let token = std::env::var("DEEPSEEK_API_KEY")?;
let client = ApiClient::new(token);

let req = ApiRequest::deepseek_chat(vec![Message::new(Role::User, "Tell me a story")])
    .stream(true);

let mut stream = client.stream_text(req).await?;
while let Some(chunk_res) = stream.next().await {
    match chunk_res {
        Ok(text) => print!("{}", text),
        Err(err) => eprintln!("Stream error: {}", err),
    }
}

Example: DeepseekConversation (auto summary)

use ds_api::{ApiClient, ApiRequest, DeepseekConversation, Message, Role};

let token = std::env::var("DEEPSEEK_API_KEY")?;
let client = ApiClient::new(token);

let mut conv = DeepseekConversation::new(client.clone())
    .with_history(vec![Message::new(Role::System, "You are a helpful assistant.")]);

conv.push_user_input("Hello! Tell me about Rust.".to_string());
let reply = conv.send_once().await?;
println!("Assistant: {:?}", reply);

Example: DeepseekAgent with tools (preferred flow)

  • Agent yields two-phase events when the model triggers tool calls:
    1. First yield: assistant content paired with tool_calls preview (result is null).
    2. Second yield: tool execution results (and these results are appended to conversation history as Role::Tool messages).

Tool functions are declared with the #[tool] macro.

use ds_api::{DeepseekAgent, tool};
use futures::StreamExt;
use serde_json::json;

struct WeatherTool {
    client: reqwest::Client,
}

#[tool]
impl Tool for WeatherTool {
    /// Get weather for a city
    async fn get_weather(&self, city: String, _unit: Option<String>) -> serde_json::Value {
        let url = format!("https://wttr.in/{}?format=3", city);
        let text = self.client.get(&url).send().await
            .and_then(|r| r.text().await)
            .unwrap_or_else(|e| e.to_string());
        json!({ "city": city, "weather": text })
    }
}

#[tokio::main]
async fn main() {
    let token = std::env::var("DEEPSEEK_API_KEY").unwrap();
    let agent = DeepseekAgent::new(token)
        .add_tool(WeatherTool { client: reqwest::Client::new() })
        .with_system_prompt("You are an assistant that can call tools.");

    let mut s = agent.chat("What's the weather in Beijing and Shanghai?");
    while let Some(event) = s.next().await {
        if let Some(content) = &event.content {
            println!("Assistant: {}", content);
        }
        for tc in &event.tool_calls {
            println!("Tool call preview/result: {} {} -> {}", tc.name, tc.args, tc.result);
        }
    }
}

Summarizer and auto-summary behavior

  • Summarizer trait: pluggable abstraction for summarization.
  • Default: TokenBasedSummarizer:
    • Rough token estimate uses chars / 4.
    • SKIPS system messages when computing the estimate.
    • Default threshold: 100_000 estimated tokens (configurable).
    • When triggered, older messages are summarized into a single system message (prefixed/marked with [auto-summary] in name).

You can implement Summarizer to call an LLM for semantic summaries or to use a different heuristic.

Migration guidance

  • Replace previous usage of Request::basic_query(...) with ApiRequest::deepseek_chat(...) or ApiRequest::builder().
  • Use ApiClient::new(token) instead of DeepseekClient.
  • Replace NormalChatter and SimpleChatter with DeepseekConversation or DeepseekAgent.
  • If you previously relied on raw accessors, migrate carefully — raw module still exists at ds_api::raw but the recommended approach is through ApiRequest and ApiClient.

Project layout (high level)

  • src/raw — raw request/response types (kept for advanced use).
  • src/api.rsApiRequest, ApiClient.
  • src/conversation/mod.rsDeepseekConversation, Summarizer + default summarizer.
  • src/agent.rsDeepseekAgent and streaming agent orchestration.
  • src/tool.rs + ds-api-macros crate — tooling macro and Tool trait.

Tests, linting, and build

  • The project uses cargo for builds, and Clippy is enforced. After refactor we run cargo clippy -p ds-api -- -D warnings as part of CI.
  • Run:
# in repo root
cargo build
cargo test
cargo clippy -p ds-api -- -D warnings

Contributing & Release notes

  • This refactor is breaking by design. A migration guide is in this README (above).
  • If you maintain downstream consumers, notify them about:
    • Removed Request/DeepseekClient, NormalChatter, SimpleChatter.
    • New preferred entry points: ApiRequest, ApiClient, DeepseekConversation, DeepseekAgent.
  • Future work:
    • Provide thin compatibility wrappers (if required).
    • Improve summarizer with a semantic LLM-backed default option (configurable).

License

Check Cargo.toml for license information.


If you want, I can:

  • Produce a dedicated UPGRADING.md with automated migration diffs.
  • Replace the example files in examples/ with simpler, up-to-date usage snippets.
  • Add a short changelog entry under docs/ describing the refactor and linking to migration tips.

Which would you prefer next?