{"id":50134467,"url":"https://github.com/minorcell/aquaregia","last_synced_at":"2026-05-23T21:01:30.862Z","repository":{"id":342061944,"uuid":"1172579595","full_name":"minorcell/aquaregia","owner":"minorcell","description":"Use Aquaregia to quickly build your Rust AI application, with a unified interface to multiple providers and powerful tool execution capabilities.","archived":false,"fork":false,"pushed_at":"2026-05-19T18:26:45.000Z","size":367,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-19T20:45:12.076Z","etag":null,"topics":["agent","ai","anthropic","google","openai","openai-compatible","rust-crates"],"latest_commit_sha":null,"homepage":"https://docs.rs/aquaregia/latest/aquaregia/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/minorcell.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-04T13:22:26.000Z","updated_at":"2026-05-19T18:27:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/minorcell/aquaregia","commit_stats":null,"previous_names":["minorcell/oxide","minorcell/aquaregia"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/minorcell/aquaregia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minorcell%2Faquaregia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minorcell%2Faquaregia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minorcell%2Faquaregia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minorcell%2Faquaregia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/minorcell","download_url":"https://codeload.github.com/minorcell/aquaregia/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minorcell%2Faquaregia/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33379721,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T01:21:08.577Z","status":"ssl_error","status_checked_at":"2026-05-23T01:20:25.255Z","response_time":265,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai","anthropic","google","openai","openai-compatible","rust-crates"],"created_at":"2026-05-23T21:01:29.711Z","updated_at":"2026-05-23T21:01:30.850Z","avatar_url":"https://github.com/minorcell.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Aquaregia\n\n**The universal AI layer for Rust.**\n\n[![Crates.io](https://img.shields.io/crates/v/aquaregia.svg)](https://crates.io/crates/aquaregia)\n[![Docs.rs](https://docs.rs/aquaregia/badge.svg)](https://docs.rs/aquaregia)\n[![License: MIT](https://img.shields.io/crates/l/aquaregia.svg)](./LICENSE)\n[![Downloads](https://img.shields.io/crates/d/aquaregia.svg)](https://crates.io/crates/aquaregia)\n\n[API Docs](https://docs.rs/aquaregia) · [Examples](./examples/README.md) · [中文文档](./README_CN.md)\n\n\u003c/div\u003e\n\nOne crate to build LLM applications and agents on a single, provider-agnostic foundation — OpenAI, Anthropic, Google, or any OpenAI-compatible endpoint.\n\n---\n\n## Quick start\n\n```rust\nuse aquaregia::{GenerateTextRequest, LlmClient};\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let client = LlmClient::openai_compatible(\"https://api.deepseek.com\")\n        .api_key(std::env::var(\"DEEPSEEK_API_KEY\")?)\n        .build()?;\n\n    let response = client\n        .generate(GenerateTextRequest::from_user_prompt(\n            \"deepseek-chat\",\n            \"Explain Rust ownership in 3 bullet points.\",\n        ))\n        .await?;\n\n    println!(\"{}\", response.output_text);\n    println!(\n        \"usage: {} → {} tokens\",\n        response.usage.input_tokens, response.usage.output_tokens\n    );\n    Ok(())\n}\n```\n\nSwap the constructor and the same call works against Anthropic, OpenAI, or Google.\n\n---\n\n## Installation\n\n```bash\ncargo add aquaregia\n```\n\nYou also need a Tokio runtime in your project.\n\n---\n\n## Providers\n\nPick a constructor; the resulting `BoundClient\u003cP\u003e` is parameterized over the provider marker `P`.\n\n| Provider          | Constructor                                              | Model argument        |\n| ----------------- | -------------------------------------------------------- | --------------------- |\n| OpenAI            | `LlmClient::openai(api_key)`                             | `\"gpt-4o\"`            |\n| Anthropic         | `LlmClient::anthropic(api_key)`                          | `\"claude-sonnet-4-5\"` |\n| Google            | `LlmClient::google(api_key)`                             | `\"gemini-2.0-flash\"`  |\n| OpenAI-compatible | `LlmClient::openai_compatible(base_url).api_key(...)`    | `\"deepseek-chat\"`     |\n\n### Client configuration\n\n```rust\nuse std::time::Duration;\n\nlet client = LlmClient::openai(std::env::var(\"OPENAI_API_KEY\")?)\n    .base_url(\"https://api.openai.com\")          // custom upstream\n    .timeout(Duration::from_secs(60))            // per-request timeout\n    .max_retries(3)                              // transient-failure retries\n    .default_max_steps(8)                        // default for Agents built from this client\n    .user_agent(\"my-app/1.0\")\n    .build()?;\n```\n\n### Typed `ModelRef\u003cP\u003e`\n\nTo prevent passing an OpenAI model name to an Anthropic client at runtime, use the typed factory helpers:\n\n```rust\nuse aquaregia::{anthropic, openai, Anthropic, ModelRef, OpenAi};\n\nlet gpt:    ModelRef\u003cOpenAi\u003e    = openai(\"gpt-4o\");\nlet claude: ModelRef\u003cAnthropic\u003e = anthropic(\"claude-sonnet-4-5\");\n\n// `client_openai.generate(GenerateTextRequest::from_user_prompt(claude, \"...\"))`\n// is a compile-time error against a `BoundClient\u003cOpenAi\u003e`.\n```\n\n`GenerateTextRequest::from_user_prompt` accepts anything implementing `IntoModelRef\u003cP\u003e` — a bare `\u0026str` for ergonomic inline calls, or the typed factories above for stronger guarantees.\n\n### OpenAI-compatible deep configuration\n\n```rust\nlet client = LlmClient::openai_compatible(\"https://api.deepseek.com\")\n    .api_key(std::env::var(\"DEEPSEEK_API_KEY\")?)\n    .header(\"x-trace-source\", \"aquaregia\")\n    .query_param(\"source\", \"sdk\")\n    .chat_completions_path(\"/v1/chat/completions\") // override the endpoint path\n    .think_tag_parsing(true)                       // parse \u003cthink\u003e...\u003c/think\u003e as reasoning\n    .think_tag_case_insensitive(true)\n    .build()?;\n```\n\n`think_tag_parsing` extracts `\u003cthink\u003e` / `\u003cthinking\u003e` blocks from the assistant message and routes them into `reasoning_parts`, matching the unified surface used by native reasoning providers.\n\n### Provider differences at a glance\n\n| Capability                       | OpenAI | Anthropic | Google | OpenAI-Compatible |\n| -------------------------------- | :----: | :-------: | :----: | :---------------: |\n| Custom `base_url`                |   ✓    |     ✓     |   ✓    |         ✓         |\n| Custom headers / query / path    |        |           |        |         ✓         |\n| `api_version` (header)           |        |     ✓     |        |                   |\n| Native reasoning content         |   ✓    |     ✓     |   ✓    |  via think tags   |\n| Tool-call streaming              |   ✓    |     ✓     |   ✓    |         ✓         |\n| Cache-token split in `Usage`     |   ✓    |     ✓     |   ✓    |   if reported     |\n\n---\n\n## Generating text\n\n### One-shot `generate`\n\n```rust\nlet response = client\n    .generate(GenerateTextRequest::from_user_prompt(\n        \"deepseek-chat\",\n        \"Summarize Rust's borrow checker for a Go developer.\",\n    ))\n    .await?;\n\nprintln!(\"{}\", response.output_text);\nprintln!(\"finish: {:?}\", response.finish_reason);\n```\n\nFull builder when you need messages, sampling, or tools:\n\n```rust\nuse aquaregia::{GenerateTextRequest, Message};\n\nlet req = GenerateTextRequest::builder(\"deepseek-chat\")\n    .message(Message::system_text(\"You are concise.\"))\n    .message(Message::user_text(\"Write a release note.\"))\n    .temperature(0.2)\n    .max_output_tokens(300)\n    .build()?;\n```\n\n### Streaming\n\n```rust\nuse aquaregia::StreamEvent;\nuse futures_util::StreamExt;\n\nlet mut stream = client.stream(request).await?;\nwhile let Some(event) = stream.next().await {\n    match event? {\n        StreamEvent::TextDelta { text }          =\u003e print!(\"{text}\"),\n        StreamEvent::ReasoningDelta { text, .. } =\u003e eprint!(\"{text}\"),\n        StreamEvent::ToolCallReady { call }      =\u003e eprintln!(\"\\n[tool] {}\", call.tool_name),\n        StreamEvent::Usage { usage }             =\u003e eprintln!(\n            \"\\nin={} out={} total={}\",\n            usage.input_tokens, usage.output_tokens, usage.total_tokens,\n        ),\n        StreamEvent::Done                        =\u003e break,\n        _ =\u003e {}\n    }\n}\n```\n\nAll variants:\n\n| Event                | Fields                                  |\n| -------------------- | --------------------------------------- |\n| `ReasoningStarted`   | `block_id`, `provider_metadata`         |\n| `ReasoningDelta`     | `block_id`, `text`, `provider_metadata` |\n| `ReasoningDone`      | `block_id`, `provider_metadata`         |\n| `TextDelta`          | `text`                                  |\n| `ToolCallReady`      | `call: ToolCall`                        |\n| `Usage`              | `usage: Usage`                          |\n| `Done`               | —                                       |\n\n### Reasoning\n\nReasoning is exposed in both sync and streaming output:\n\n```rust\nlet out = client.generate(req).await?;\n\nprintln!(\"answer:     {}\", out.output_text);\nprintln!(\"thinking:   {}\", out.reasoning_text);\nprintln!(\"rsn-tokens: {}\", out.usage.reasoning_tokens);\n\nfor part in \u0026out.reasoning_parts {\n    println!(\"[block] {}\", part.text);\n    // part.provider_metadata carries signature blocks (Anthropic), thought\n    // signatures (Google), and provider-specific extras.\n}\n```\n\n| Provider                   | Reasoning content                                                              | Usage mapping                                                                                       |\n| -------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |\n| OpenAI / OpenAI-compatible | `reasoning_content` (or `reasoning`); `\u003cthink\u003e` tags if enabled                | parses `prompt_tokens_details.cached_tokens` + `completion_tokens_details.reasoning_tokens`         |\n| Anthropic                  | `thinking` / `redacted_thinking`; stream `thinking_delta` + `signature_delta`  | parses `cache_read_input_tokens` / `cache_creation_input_tokens`; reasoning split unavailable       |\n| Google                     | parts with `thought: true`, optional `thoughtSignature` metadata               | parses `cachedContentTokenCount` + `thoughtsTokenCount`                                             |\n\n### `Usage` and aggregation\n\n```rust\npub struct Usage {\n    pub input_tokens:             u32, // total\n    pub input_no_cache_tokens:    u32,\n    pub input_cache_read_tokens:  u32,\n    pub input_cache_write_tokens: u32,\n    pub output_tokens:            u32, // total\n    pub output_text_tokens:       u32,\n    pub reasoning_tokens:         u32,\n    pub total_tokens:             u32,\n    pub raw_usage:                Option\u003cserde_json::Value\u003e,\n}\n```\n\n`Usage` implements `Add` and `AddAssign`, so totaling tokens across agent steps is a one-liner. `AgentResponse.usage_total` is already aggregated for you.\n\n---\n\n## Tools \u0026 Agents\n\n### Defining tools\n\nTools are built with the `tool(name)` function (there is no `#[tool]` proc-macro). Two execution styles are supported.\n\n**Typed args** — `schemars` derives the JSON Schema from your struct:\n\n```rust\nuse aquaregia::{Tool, tool};\nuse schemars::JsonSchema;\nuse serde::Deserialize;\nuse serde_json::json;\n\n#[derive(Debug, Deserialize, JsonSchema)]\nstruct WeatherArgs { city: String }\n\nfn get_weather() -\u003e Tool {\n    tool(\"get_weather\")\n        .description(\"Get weather by city\")\n        .execute(|args: WeatherArgs| async move {\n            Ok(json!({ \"city\": args.city, \"temp_c\": 23, \"condition\": \"sunny\" }))\n        })\n}\n```\n\n**Raw schema** — write the JSON Schema by hand, receive a `serde_json::Value`:\n\n```rust\nlet fx_tool = tool(\"get_fx_rate\")\n    .description(\"Get FX rate by currency pair, e.g. USD/CNY\")\n    .raw_schema(json!({\n        \"type\": \"object\",\n        \"properties\": { \"pair\": { \"type\": \"string\" } },\n        \"required\": [\"pair\"]\n    }))\n    .execute_raw(|args| async move {\n        let pair = args.get(\"pair\").and_then(|v| v.as_str()).unwrap_or(\"USD/CNY\");\n        Ok(json!({ \"pair\": pair, \"rate\": 7.18 }))\n    });\n```\n\nTool names must match `^[a-zA-Z0-9_-]{1,64}$` and be unique within an agent.\n\n### Minimal Agent\n\n```rust\nuse aquaregia::{Agent, LlmClient};\n\nlet client = LlmClient::openai_compatible(\"https://api.deepseek.com\")\n    .api_key(std::env::var(\"DEEPSEEK_API_KEY\")?)\n    .build()?;\n\nlet agent = Agent::builder(client, \"deepseek-chat\")\n    .instructions(\"You can call tools before answering.\")\n    .tools([get_weather])\n    .max_steps(4)\n    .build()?;\n\nlet response = agent.run(\"Weather in Shanghai?\").await?;\nprintln!(\"{}\", response.output_text);\nprintln!(\"steps={} total={}\", response.steps, response.usage_total.total_tokens);\n```\n\n### Event hooks\n\nThe agent loop emits an event at every meaningful boundary. All hooks are `Fn + Send + Sync`, so you can attach them as closures.\n\n```rust\nlet agent = Agent::builder(client, \"deepseek-chat\")\n    .tools([get_weather])\n    .on_start(|e|            println!(\"[start] tools={} max_steps={}\", e.tool_count, e.max_steps))\n    .on_step_start(|e|       println!(\"[step:{}] msgs={}\", e.step, e.messages.len()))\n    .on_tool_call_start(|e|  println!(\"[tool:{}] {}\", e.step, e.tool_call.tool_name))\n    .on_tool_call_finish(|e| println!(\"[tool:{}] {} in {}ms\", e.step, e.tool_call.tool_name, e.duration_ms))\n    .on_step_finish(|s|      println!(\"[step:{}] finish={:?}\", s.step, s.finish_reason))\n    .on_finish(|f|           println!(\"[done] {} steps, {} total tokens\", f.step_count, f.usage_total.total_tokens))\n    .build()?;\n```\n\n### Dynamic planning — `prepare_step`\n\n`prepare_step` runs before every step and returns a fresh prepared plan — useful for shrinking the tool set, switching models, or injecting per-step instructions:\n\n```rust\nuse aquaregia::Message;\n\nlet agent = Agent::builder(client, \"deepseek-chat\")\n    .tools([get_weather, get_fx_rate])\n    .prepare_step(|event| {\n        let mut next = event.to_prepared();\n        next.messages.push(Message::system_text(format!(\n            \"Step {}: be concise.\", event.step,\n        )));\n        if event.step \u003e= 2 {\n            next.tools.clear(); // disallow tools after step 2\n        }\n        next\n    })\n    .build()?;\n```\n\n### Stopping policies\n\n```rust\nuse aquaregia::ToolErrorPolicy;\n\nlet agent = Agent::builder(client, \"deepseek-chat\")\n    .max_steps(8)                                                                 // hard cap\n    .stop_when(|step| step.tool_calls.is_empty() \u0026\u0026 !step.output_text.is_empty()) // predicate\n    .tool_error_policy(ToolErrorPolicy::ContinueAsToolResult)                     // default\n    .build()?;\n```\n\n- `max_steps` — exceeding it returns `ErrorCode::MaxStepsExceeded`.\n- `stop_when` — predicate evaluated after every step; truthy = stop early.\n- `tool_error_policy` —\n  - `ContinueAsToolResult` (default) — schema-validation failures, timeouts, and panics become `{ \"error\": \"...\" }` tool results so the model can recover.\n  - `FailFast` — surface as `ErrorCode::ToolExecutionFailed` / `InvalidToolArgs` immediately.\n\n### Multi-turn conversations\n\n`AgentResponse.transcript` is a complete `Vec\u003cMessage\u003e` (system + user + assistant + tool results) that you can feed straight back into the next turn:\n\n```rust\nlet mut history = vec![Message::system_text(\"You are a careful assistant.\")];\n\nloop {\n    let user_input = read_line()?;\n    history.push(Message::user_text(user_input));\n\n    let result = agent.run_messages(history.clone()).await?;\n    println!(\"{}\", result.output_text);\n\n    history = result.transcript; // round-trip the full conversation\n}\n```\n\nSee `examples/mini_claude_code.rs` for a working terminal agent that uses this pattern with `bash` / `read` / `write` / `edit` tools.\n\n---\n\n## Multimodal vision\n\n```rust\nuse aquaregia::{\n    ContentPart, GenerateTextRequest, ImagePart, LlmClient, MediaData, Message, MessageRole,\n};\n\nlet client = LlmClient::anthropic(std::env::var(\"ANTHROPIC_API_KEY\")?).build()?;\n\nlet out = client\n    .generate(\n        GenerateTextRequest::builder(\"claude-sonnet-4-5\")\n            .message(Message::new(\n                MessageRole::User,\n                vec![\n                    ContentPart::Text(\"What's in this image?\".into()),\n                    ContentPart::Image(ImagePart {\n                        data: MediaData::Url(\n                            \"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg\".into(),\n                        ),\n                        media_type: None,\n                        provider_metadata: None,\n                    }),\n                ],\n            )?)\n            .build()?,\n    )\n    .await?;\n```\n\nTwo convenience constructors plus a full-control form:\n\n| Constructor                                                              | Use case                                  |\n| ------------------------------------------------------------------------ | ----------------------------------------- |\n| `Message::user_image_url(url)`                                           | Single image from a URL                   |\n| `Message::user_image_bytes(bytes, mime)`                                 | Single image from raw bytes (auto base64) |\n| `Message::new(MessageRole::User, vec![Text, Image, …])`                  | Mixed content (text + image, multi-image) |\n| `ContentPart::Image(ImagePart { data, media_type, provider_metadata })`  | Full control + provider-specific hints    |\n\nEach provider sees its own native format:\n\n| Provider            | URL                          | Base64 / Bytes                          |\n| ------------------- | ---------------------------- | --------------------------------------- |\n| Anthropic           | `source.type: url`           | `source.type: base64`                   |\n| OpenAI / Compatible | `image_url` with remote URL  | `image_url` with `data:\u003cmime\u003e;base64,…` |\n| Google              | `fileData.fileUri`           | `inlineData.data`                       |\n\n---\n\n## Cancellation\n\nEvery request and agent run is cancellable through a `CancellationToken`.\n\n```rust\nuse aquaregia::{CancellationToken, ErrorCode, GenerateTextRequest};\nuse std::time::Duration;\n\nlet token = CancellationToken::new();\nlet bg = token.clone();\ntokio::spawn(async move {\n    tokio::time::sleep(Duration::from_millis(200)).await;\n    bg.cancel();\n});\n\nlet req = GenerateTextRequest::builder(\"deepseek-chat\")\n    .user_prompt(\"Write a 10,000-word essay.\")\n    .cancellation_token(token)\n    .build()?;\n\nmatch client.generate(req).await {\n    Err(e) if e.code == ErrorCode::Cancelled =\u003e println!(\"cancelled\"),\n    other =\u003e println!(\"{other:?}\"),\n}\n```\n\nAgents bind the token at builder time:\n\n```rust\nlet agent = Agent::builder(client, \"deepseek-chat\")\n    .cancellation_token(token.clone())\n    .build()?;\n\nagent.run(\"hello\").await?;\nagent.run_messages(messages).await?;\n```\n\nCancellation is checked **before every HTTP send** (via `tokio::select!`, zero overhead on the happy path), **after every SSE chunk** in streaming responses, and **at the top of every agent step** in the tool loop.\n\n---\n\n## Reliability\n\n### Retries\n\n```rust\nlet client = LlmClient::openai(api_key)\n    .max_retries(3)                       // default: 0\n    .timeout(Duration::from_secs(45))\n    .build()?;\n```\n\nAquaregia retries automatically on transient classes (`RateLimited`, `ProviderServerError`, `Transport`, `Timeout`) using exponential backoff with jitter. The `Retry-After` header is parsed and honored when present.\n\nEvery `Error` carries a `retryable: bool` flag matching the same classification, so you can layer your own retry/circuit-breaker on top if you need finer control.\n\n---\n\n## Framework integration example (Axum)\n\nAquaregia intentionally keeps web framework adapters out of the crate. If you're building on Axum, adapt `TextStream` in your application layer:\n\n```rust\nuse aquaregia::{BoundClient, GenerateTextRequest, OpenAiCompatible, StreamEvent, TextStream};\nuse axum::{\n    extract::State,\n    response::{\n        IntoResponse,\n        sse::{Event, Sse},\n    },\n    routing::get,\n    Router,\n};\nuse futures_util::StreamExt;\nuse std::{convert::Infallible, sync::Arc};\n\nfn to_axum_sse(\n    stream: TextStream,\n) -\u003e impl IntoResponse {\n    Sse::new(stream.map(|item| {\n        let event = match item {\n            Ok(StreamEvent::ReasoningStarted { .. }) =\u003e {\n                Event::default().event(\"reasoning_start\").data(\"{}\")\n            }\n            Ok(StreamEvent::ReasoningDelta { text, .. }) =\u003e {\n                Event::default().event(\"reasoning_token\").data(text)\n            }\n            Ok(StreamEvent::ReasoningDone { .. }) =\u003e {\n                Event::default().event(\"reasoning_end\").data(\"{}\")\n            }\n            Ok(StreamEvent::TextDelta { text }) =\u003e Event::default().event(\"token\").data(text),\n            Ok(StreamEvent::ToolCallReady { .. }) =\u003e Event::default().event(\"tool_call\").data(\"{}\"),\n            Ok(StreamEvent::Usage { .. }) =\u003e Event::default().event(\"usage\").data(\"{}\"),\n            Ok(StreamEvent::Done) =\u003e Event::default().event(\"done\").data(\"{}\"),\n            Err(err) =\u003e Event::default().event(\"error\").data(err.message),\n        };\n        Ok::\u003cEvent, Infallible\u003e(event)\n    }))\n}\n\nasync fn chat(State(client): State\u003cArc\u003cBoundClient\u003cOpenAiCompatible\u003e\u003e\u003e) -\u003e impl IntoResponse {\n    let stream = client\n        .stream(GenerateTextRequest::from_user_prompt(\"deepseek-chat\", \"Hello.\"))\n        .await\n        .unwrap();\n    to_axum_sse(stream)\n}\n\nlet app: Router = Router::new()\n    .route(\"/chat\", get(chat))\n    .with_state(Arc::new(client));\n```\n\nThe example keeps non-text payloads minimal; in a real app, serialize tool calls, usage, and reasoning metadata into whatever wire format your frontend expects.\n\nMap the `StreamEvent` variants you care about to named SSE events, websocket messages, or any other transport format your app uses.\n\n---\n\n## Error handling\n\n```rust\nuse aquaregia::ErrorCode;\n\nmatch client.generate(req).await {\n    Ok(out) =\u003e println!(\"{}\", out.output_text),\n    Err(e) =\u003e match e.code {\n        ErrorCode::RateLimited        =\u003e eprintln!(\"retry after {:?}s\", e.retry_after_secs),\n        ErrorCode::AuthFailed         =\u003e eprintln!(\"bad API key\"),\n        ErrorCode::Cancelled          =\u003e eprintln!(\"cancelled\"),\n        ErrorCode::MaxStepsExceeded   =\u003e eprintln!(\"agent loop too long\"),\n        ErrorCode::InvalidToolArgs    =\u003e eprintln!(\"schema mismatch: {}\", e.message),\n        ErrorCode::Timeout            =\u003e eprintln!(\"upstream timed out\"),\n        _                              =\u003e eprintln!(\"error: {e}\"),\n    },\n}\n```\n\nEvery `Error` carries:\n\n- `code: ErrorCode` — one of 13 normalized variants\n- `provider`, `status`, `request_id`, `raw_body`, `retry_after_secs` — for logging and triage\n- `retryable: bool` — `true` iff Aquaregia's built-in retry will engage\n\n---\n\n## Examples\n\n```bash\nDEEPSEEK_API_KEY=... cargo run --example basic_generate\n```\n\n| Example                       | Focus                                                       |\n| ----------------------------- | ----------------------------------------------------------- |\n| `basic_generate`              | One-shot `generate` + usage reading                         |\n| `basic_stream`                | `stream` + `StreamEvent` handling                           |\n| `agent_minimal`               | `Agent::builder` with one typed tool                        |\n| `tools_max_steps`             | Multi-tool loop with `max_steps` and sampling caps          |\n| `prepare_hooks`               | `prepare_step`, `on_step_finish`                            |\n| `openai_compatible_custom`    | Custom headers / query params / chat path                   |\n| `mini_claude_code`            | TUI code agent — `bash` / `read` / `write` / `edit` tools   |\n| `multimodal_image`            | `Message::new` with mixed text + image parts + Anthropic vision |\n\nSet `DEEPSEEK_API_KEY` for most examples; `ANTHROPIC_API_KEY` for `multimodal_image`. See [`examples/README.md`](./examples/README.md) for full descriptions.\n\n---\n\n## Development\n\n```bash\ncargo fmt\ncargo test\ncargo check --examples\ncargo clippy -- -D warnings\n```\n\n---\n\n## AI-Assisted Development\n\nAI-assisted development is welcome in this project, but the contributor remains responsible for the final result. If code, tests, docs, or API changes are proposed with AI help, the person submitting them is still expected to understand, review, and validate them.\n\nThis repository also keeps agent-facing guidance principle-based on purpose. Files such as `AGENTS.md` and `CLAUDE.md` should describe durable constraints and decision rules, not long checklists of internal APIs that drift away from the code.\n\n---\n\n## Contributing \u0026 License\n\nContributions are welcome. For behavior changes, include integration tests (happy path + error mapping + tool/stream flows where relevant).\n\n- [Contributing Guide](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Security Policy](./SECURITY.md)\n- Licensed under the [MIT License](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminorcell%2Faquaregia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fminorcell%2Faquaregia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminorcell%2Faquaregia/lists"}