概览
kotoba 是一个 Rust 编写的“统一 LLM 调用层”,通过一套共享的 Message、ContentPart、ChatRequest、ChatResponse 等类型,将 OpenAI Chat、OpenAI Responses、Anthropic Messages、Google Gemini 等主流厂商封装进统一接口。仓库暴露的 LLMClient、LLMProvider 与 HttpTransport 抽象允许你:
- 在同一进程内注册多个 Provider 句柄,并通过 handle 名称路由请求;
- 复用一致的多模态消息建模,涵盖文本、图片、音频、视频、文件、工具调用以及流式增量事件;
- 在不改变业务代码的情况下切换 HTTP 实现、注入 mock、或扩展自定义 Provider;
- 借助统一错误
LLMError和能力矩阵CapabilityDescriptor,对不同后端进行特性检测和熔断处理。
快速上手
1. 引入依赖
在业务 crate 的 Cargo.toml 中通过 crates.io 引入:
[dependencies]
kotoba-llm = "0.2.0"
2. 构造 LLMClient
LLMClient 通过 builder 注册多个 Provider 句柄。下面演示如何以 OpenAI Chat 为例建立同步与流式请求。
use std::sync::Arc; use futures_util::StreamExt; use kotoba_llm::{LLMClient, LLMError}; use kotoba_llm::http::reqwest::default_dyn_transport; use kotoba_llm::provider::openai_chat::OpenAiChatProvider; use kotoba_llm::types::{ChatRequest, Message, Role, ContentPart, TextContent}; #[tokio::main] async fn main() -> Result<(), LLMError> { let transport = default_dyn_transport()?; let provider = OpenAiChatProvider::new(transport.clone(), std::env::var("OPENAI_API_KEY")?) .with_default_model("gpt-4.1-mini"); let client = LLMClient::builder() .register_handle("openai", Arc::new(provider))? .build(); let request = ChatRequest { messages: vec![Message { role: Role::user(), name: None, content: vec![ContentPart::Text(TextContent { text: "请用一句话介绍你自己".into(), })], metadata: None, }], options: Default::default(), tools: Vec::new(), tool_choice: None, response_format: None, metadata: None, }; let response = client.chat("openai", request.clone()).await?; println!("同步结果: {:?}", response.outputs); let mut stream = client.stream_chat("openai", request).await?; while let Some(chunk) = stream.next().await { let chunk = chunk?; // chunk.events 中携带 text delta / tool delta 等统一事件 println!("delta = {:?}", chunk.events); } Ok(()) }
3. 运行期能力筛选
client.capabilities(handle)返回当前句柄的CapabilityDescriptor,可在运行前检查是否支持流式、多模态或工具。client.handles_supporting_stream()与client.handles_supporting_tools()直接返回满足条件的 handle 列表,便于按能力路由请求。
4. 统一配置装载
若需要从配置文件或数据库批量注册 Provider,可使用 config::build_client_from_configs:
use kotoba_llm::config::{ModelConfig, ProviderKind, Credential, build_client_from_configs}; use kotoba_llm::types::{ChatRequest, Message, Role, ContentPart, TextContent}; use kotoba_llm::http::reqwest::default_dyn_transport; #[tokio::main] async fn main() -> Result<(), kotoba_llm::LLMError> { let configs = vec![ModelConfig { handle: "claude".into(), provider: ProviderKind::AnthropicMessages, credential: Credential::ApiKey { header: None, key: std::env::var("ANTHROPIC_KEY")? }, default_model: Some("claude-3-5-sonnet".into()), base_url: None, extra: [ ("version".into(), serde_json::json!("2023-06-01")), ("beta".into(), serde_json::json!("client-tools")) ].into_iter().collect(), patch: None, }]; let transport = default_dyn_transport()?; let client = build_client_from_configs(&configs, transport)?; let request = ChatRequest { messages: vec![Message { role: Role::user(), name: None, content: vec![ContentPart::Text(TextContent { text: "讲一个笑话".into() })], metadata: None, }], options: Default::default(), tools: Vec::new(), tool_choice: None, response_format: None, metadata: None, }; let _ = client.chat("claude", request).await?; Ok(()) }
核心类型与架构
模块速览
| 模块 | 作用 |
|---|---|
src/types | 定义 Role、Message、ContentPart 到 ChatRequest、ChatResponse、ChatChunk 的全量数据结构,覆盖文本、多模态、工具、推理与流式事件。 |
src/provider | 暴露 LLMProvider trait 与具体供应商实现,负责将统一模型映射为厂商 API 请求并解析响应。 |
src/client | 提供 LLMClient 与 LLMClientBuilder,路由 handle → Provider,支持能力查询及工具/流式筛选。 |
src/config | 用 ModelConfig/ProviderKind/Credential 表示外部配置,并提供 build_client_from_configs 批量注册 Provider。 |
src/http | 定义轻量 HttpTransport 抽象与 ReqwestTransport 默认实现,便于切换或注入 mock。 |
src/error | 聚合所有错误为 LLMError,并提供 transport()、provider() 等便捷构造。 |
多模态与工具建模
Message 通过 Vec<ContentPart> 支持以下类型:
- 文本:
ContentPart::Text(TextContent); - 图片:
ImageContent支持 URL、Base64、文件 ID,并带ImageDetail枚举; - 音频 / 视频:
AudioContent、VideoContent结合MediaSource::Inline/FileId/Url; - 文件引用、原始 JSON 数据;
- 工具调用 (
ToolCall) 与工具结果 (ToolResult),二者在不同 Provider 中被各自的 request mapper 处理。
ChatRequest 附带 ChatOptions(温度、top_p、max_output_tokens、penalty、parallel_tool_calls、ReasoningOptions、extra),并行工具策略由 ToolChoice 决定,输出格式通过 ResponseFormat 声明(文本 / JSON / JSON Schema / 自定义)。
ChatResponse 统一封装 OutputItem(消息、工具、工具结果、推理文本、自定义 payload)、TokenUsage、FinishReason 及 ProviderMetadata。流式场景使用 ChatChunk + ChatEvent 描述增量文本/工具 delta,保持与同步响应相同的语义。
Provider 抽象
#![allow(unused)] fn main() { #[async_trait] pub trait LLMProvider: Send + Sync { async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, LLMError>; async fn stream_chat(&self, request: ChatRequest) -> Result<ChatStream, LLMError>; fn capabilities(&self) -> CapabilityDescriptor; fn name(&self) -> &'static str; } }
每个 Provider 模块拆成 provider.rs(实现 trait)、request.rs(构建 JSON)、response.rs(解析响应为统一类型)、stream.rs(SSE/Chunk 解析)、error.rs(HTTP 错误解析)、types.rs(中间结构)。这种分层让新增 Provider 只需补齐映射逻辑即可。
CapabilityDescriptor 用于声明 supports_stream、supports_image_input 等能力,LLMClient 通过它进行 handle 过滤。
客户端路由
LLMClient 内部维护 HashMap<String, DynProvider>:
chat(handle, request)与stream_chat(handle, request)只负责定位 Provider 并转发;handles()返回 handle 列表;capabilities(handle)获取特性描述;handles_supporting_tools()/handles_supporting_stream()根据CapabilityDescriptor自动筛选。
LLMClientBuilder 提供 register_handle(handle, Arc<dyn LLMProvider>),在 build() 时做重复 handle 校验并返回可用客户端。测试中可以注入简单的 LLMProvider stub 验证路由逻辑。
配置与凭证
ModelConfig 暴露以下字段:handle、provider、credential、default_model、base_url、extra。ProviderKind 枚举包含四个当前实现。Credential 支持 ApiKey(可自定义 header)、Bearer、ServiceAccount(仅做校验,当前 Provider 均不接受)、None。build_client_from_configs 按序构造 Provider 并注册 handle,遇到缺少凭证、重复 handle、或 Service Account 等不被支持的 credential 时抛出 LLMError::Auth/Validation。
HTTP 抽象
HttpTransport 提供 send(HttpRequest) -> HttpResponse 与 send_stream(HttpRequest) -> HttpStreamResponse。默认的 ReqwestTransport 实现在 http/reqwest.rs,但调用方可注入自定义实现以满足:
- 统一代理/超时/重试策略;
- 在测试中返回固定 JSON 或模拟 SSE。
post_json_with_headers、post_json_stream_with_headers 小工具隐藏了重复的 header 注入逻辑,使 Provider 实现专注于业务映射。
错误处理
所有错误通过 LLMError 传播:
- 网络层:
Transport; - 鉴权:
Auth; - 速率限制:
RateLimit(附带retry_after); - Token/上下文溢出:
TokenLimitExceeded(保留原始消息); - 模型缺失或不可用:
ModelNotFound; - 运行时请求校验:
Validation; - 配置阶段错误:
InvalidConfig; - 用户主动取消:
Aborted; - SSE 早退:
StreamClosed(含原始错误信息); - 能力不支持:
UnsupportedFeature; - 厂商返回:
Provider; - 预留占位:
NotImplemented、Unknown。
在 Provider 内可以直接使用 LLMError::transport() 和 LLMError::provider() 保持信息格式一致,便于上层日志与重试策略集中处理。
客户端与配置装载
config::build_client_from_configs 让你在启动阶段就把所有 Provider 注册好,避免在业务代码里散落大量构造逻辑。本章说明配置字段及各 Provider 支持的额外键。
ModelConfig 字段
| 字段 | 说明 |
|---|---|
handle | 注册到 LLMClient 的唯一名称,后续 client.chat(handle, ..) 通过它路由。重复 handle 会立即触发 LLMError::InvalidConfig。 |
provider | ProviderKind 枚举,当前支持 OpenAiChat、OpenAiResponses、AnthropicMessages、GoogleGemini。 |
credential | Credential::ApiKey { header, key }、Credential::Bearer { token }、Credential::ServiceAccount { json }、Credential::None。除 ServiceAccount/None 外都会映射到对应 Provider;不满足条件时返回 LLMError::Auth。 |
default_model | 当 ChatRequest.options.model 为空时的兜底模型。绝大多数 Provider 都在请求阶段要求模型,缺失会报 LLMError::Validation。 |
base_url | 可选的自定义地址,便于本地代理或企业网关。构造时会调用 Provider 的 with_base_url。 |
extra | HashMap<String, Value>,按 Provider 约定解析。未知键会被忽略。 |
patch | 可选的 RequestPatch,用于在运行时修改请求 URL、Headers 或 Body。详见"请求补丁"章节。 |
Credential 注意事项
- OpenAI 与 Anthropic/Gemini 均要求 API Key 或 Bearer Token;
Credential::None会触发LLMError::Auth。 Credential::ServiceAccount当前尚未被任何 Provider 支持,会直接返回LLMError::Auth,以免让调用者误以为可以使用 JSON 凭证。- 可以用
header字段覆盖默认 header 名,例如某些代理要求X-API-Key。若为空,代码会按 Provider 既定 header(Authorization,x-api-key,x-goog-api-key等)填写。
extra 字段约定
| Provider | extra 键 | 作用 |
|---|---|---|
| OpenAiChat / OpenAiResponses | organization、project | 分别映射到 OpenAI-Organization、OpenAI-Project header。 |
| AnthropicMessages | version、beta | 映射到 anthropic-version 与 anthropic-beta header,用逗号分隔多个 beta。 |
| GoogleGemini | (暂无默认键) | 可以自定义,例如 safetySettings、cachedContent,直接透传到请求 JSON。 |
使用示例
#![allow(unused)] fn main() { use kotoba_llm::config::{ModelConfig, ProviderKind, Credential, build_client_from_configs}; use kotoba_llm::http::reqwest::default_dyn_transport; fn load_client() -> Result<kotoba_llm::LLMClient, kotoba_llm::LLMError> { let mut extras = std::collections::HashMap::new(); extras.insert("organization".to_string(), serde_json::json!("org_123")); let configs = vec![ModelConfig { handle: "openai-primary".into(), provider: ProviderKind::OpenAiChat, credential: Credential::ApiKey { header: None, key: std::env::var("OPENAI_KEY")? }, default_model: Some("gpt-4.1-mini".into()), base_url: None, extra: extras, patch: None, }]; let transport = default_dyn_transport()?; build_client_from_configs(&configs, transport) } }
常见错误与防御
| 问题 | 表现 | 解决办法 |
|---|---|---|
忘记在配置里设置 default_model | Provider 构造成功,但在运行时调用 chat 会因缺少模型返回 LLMError::Validation | 在配置层约束必须指定 default_model,或在业务层始终给 ChatRequest.options.model 赋值。 |
extra 键误拼写 | 不会报错,但相应 header/字段不会生效 | 在配置 schema 或集成测试中校验常见键是否存在。 |
| 同一个 handle 重复出现在配置里 | build_client_from_configs 会直接报 LLMError::InvalidConfig { field: "handle", reason: "duplicate model handle: ..." } | 在生成配置时先做去重,或按照 Provider 目的命名(如 openai-fallback)。 |
使用 Credential::ServiceAccount 配置 Gemini | 当前实现直接拒绝,提示"provider google_gemini does not support service account credential" | 改用 API Key 或在外部服务中交换为 Bearer token 后再注入。 |
请求补丁 (Request Patch)
RequestPatch 允许你在不修改代码的情况下对 Provider 的 HTTP 请求进行运行时修改,适用于以下场景:
- 添加自定义参数:向请求 body 添加厂商特有的实验性参数
- 修改请求地址:覆盖 Provider 的默认 endpoint URL
- 自定义 HTTP Headers:添加额外的 header 或删除不需要的 header
- 移除不兼容字段:删除某些 Provider 不支持的字段
RequestPatch 字段
| 字段 | 类型 | 说明 |
|---|---|---|
url | Option<String> | 覆盖请求的完整 URL |
body | Option<Value> | 深度合并到请求 body 中的 JSON 对象 |
headers | Option<HashMap<String, Option<String>>> | 添加/覆盖 HTTP headers。值为 None 时删除该 header |
remove_fields | Option<Vec<String>> | 按点分路径删除 body 中的字段,支持数组下标 |
使用示例
添加自定义参数
#![allow(unused)] fn main() { use kotoba_llm::config::{ModelConfig, RequestPatch, ProviderKind, Credential}; use serde_json::json; let patch = RequestPatch { url: None, body: Some(json!({ "frequency_penalty": 0.5, "presence_penalty": 0.3, "user": "kotoba-client" })), headers: None, remove_fields: None, }; let config = ModelConfig { handle: "openai-custom".into(), provider: ProviderKind::OpenAiChat, credential: Credential::ApiKey { header: None, key: std::env::var("OPENAI_KEY").unwrap() }, default_model: Some("gpt-4".into()), base_url: None, extra: std::collections::HashMap::new(), patch: Some(patch), }; }
修改请求地址和添加自定义 Header
#![allow(unused)] fn main() { use std::collections::HashMap; let patch = RequestPatch { url: Some("https://my-proxy.local/v1/chat/completions".into()), body: None, headers: Some([ ("X-Custom-Header".to_string(), Some("my-value".to_string())), ("X-Debug".to_string(), None), // 删除 X-Debug header ].into_iter().collect()), remove_fields: None, }; }
移除不支持的字段
某些自定义 OpenAI 兼容服务可能不支持 parallel_tool_calls 参数:
#![allow(unused)] fn main() { let patch = RequestPatch { url: None, body: None, headers: None, remove_fields: Some(vec![ "parallel_tool_calls".to_string(), "metadata.trace_id".to_string(), // 支持嵌套路径 ]), }; }
综合示例
#![allow(unused)] fn main() { let patch = RequestPatch { url: Some("https://custom-api.local/chat".into()), body: Some(json!({ "temperature": 0.7, "top_p": 0.9, "custom_field": { "experiment": "A", "trace": true } })), headers: Some([ ("X-Experiment-Id".to_string(), Some("exp-123".to_string())), ("X-Old-Header".to_string(), None), ].into_iter().collect()), remove_fields: Some(vec!["stream_options".to_string()]), }; }
补丁应用顺序
补丁按以下顺序应用到请求中:
- URL 替换:如果
patch.url存在,覆盖原始 URL - Body 深度合并:将
patch.body递归合并到请求 body 中 - Headers 修改:添加/覆盖/删除 HTTP headers
- 字段移除:从 body 中删除指定路径的字段
注意事项
- 深度合并:
body中的对象会递归合并,不会简单覆盖整个对象 - 类型替换:如果 patch 中的值类型与原值不同,会完全替换
- 路径语法:
remove_fields使用点分路径,例如metadata.user_id或messages.0.content - 兼容性:所有 Provider (OpenAI Chat/Responses, Anthropic Messages, Google Gemini) 都支持补丁功能
通过集中配置并统一校验,可以确保业务代码聚焦在 ChatRequest 构造与结果处理上,同时降低多环境部署时的差异。
HTTP 传输与测试
抽象层
src/http/mod.rs 定义的 HttpTransport trait 只有两个异步方法:
#![allow(unused)] fn main() { #[async_trait] pub trait HttpTransport: Send + Sync { async fn send(&self, request: HttpRequest) -> Result<HttpResponse, LLMError>; async fn send_stream(&self, request: HttpRequest) -> Result<HttpStreamResponse, LLMError>; } }
HttpRequest记录 method、url、headers、body、timeout;HttpResponse/HttpStreamResponse提供into_string()、HttpBodyStream等便捷方法;- 工具函数
post_json_with_headers、post_json_stream_with_headers帮助 Provider 构造POST + JSON请求。
默认实现
ReqwestTransport(src/http/reqwest.rs)封装 reqwest::Client,负责:
- 根据
HttpMethod构建请求并拷贝 header/body; - 在
send_stream中把bytes_stream转换成HttpBodyStream,并把reqwest::Error统一映射为LLMError::Transport; default_dyn_transport()返回Arc<ReqwestTransport>,日常使用时直接传给 Provider 即可。
Mock 与测试
由于 Provider 只依赖 DynHttpTransport,可以在测试中注入伪实现:
#![allow(unused)] fn main() { use async_trait::async_trait; use futures_util::stream; use kotoba_llm::http::{HttpTransport, HttpRequest, HttpResponse, HttpStreamResponse, HttpBodyStream}; use kotoba_llm::error::LLMError; struct MockTransport; #[async_trait] impl HttpTransport for MockTransport { async fn send(&self, request: HttpRequest) -> Result<HttpResponse, LLMError> { assert!(request.url.contains("chat/completions")); Ok(HttpResponse { status: 200, headers: Default::default(), body: br#"{\"id\":\"mock\"}"#.to_vec(), }) } async fn send_stream(&self, _request: HttpRequest) -> Result<HttpStreamResponse, LLMError> { let stream = stream::once(async { Ok(br"data: [DONE]\n\n".to_vec()) }); Ok(HttpStreamResponse { status: 200, headers: Default::default(), body: Box::pin(stream) as HttpBodyStream, }) } } }
借助 mock 可以:
- 验证请求 JSON 是否包含期望字段(通过深拷贝
HttpRequest.body); - 为
stream_chat提供自定义 SSE 序列,确保流式解析逻辑的单元测试不依赖公网; - 在回归测试里注入故障(如返回 429 或畸形 JSON)以覆盖错误分支。
超时与重试
HttpRequest 自带 timeout: Option<Duration> 字段,Provider 可以在构造请求时填入(目前默认留空,由上层 HTTP 客户端控制)。如果需要跨 Provider 统一设置,推荐:
- 自定义
reqwest::Client,在 builder 中配置超时与代理,然后传入ReqwestTransport::new(client); - 或者实现一个包装器,在调用
inner.send()前设置request.timeout、注入 trace header、记录 metrics。
这样既不会污染 Provider 的业务逻辑,也让网络策略集中可控。
Provider 指南
本章聚焦四个内置 Provider 的特性、差异与使用场景。无论是直接手动构造 Provider,还是通过 ModelConfig 装载,都可以参考以下维度挑选:
能力矩阵
| Provider | Streaming | 图像输入 | 音频输入 | 视频输入 | 工具调用 | 结构化输出 | 并行工具 |
|---|---|---|---|---|---|---|---|
OpenAI Chat (openai_chat) | ✅ | ✅ | ✅ | ⚠️(CapabilityDescriptor 暂未宣称) | ✅ | ✅ | ✅ |
OpenAI Responses (openai_responses) | ✅ | ✅ | ⚠️(暂未宣称) | ⚠️ | ✅(Function/File/Web/Computer) | ✅ | ✅ |
Anthropic Messages (anthropic_messages) | ✅ | ✅(仅支持 base64 图像) | ❌ | ❌ | ✅ | ⚠️(尚未公开 JSON 模式) | ✅ |
Google Gemini (google_gemini) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅(含 JSON Schema) | ✅ |
⚠️ 表示当前
CapabilityDescriptor中标记为false,即便请求映射支持对应字段,也会谨慎地对外宣告“未正式支持”。
后续章节将深入每个 Provider 的构造、请求映射、Streaming 与调试细节。
OpenAI Chat
适用场景
- 对接 OpenAI Chat Completions 或兼容代理(如
https://api.openai.com/v1/chat/completions); - 需要标准 function tool 调用、JSON 模式、流式 SSE;
- 希望在请求体中同时发送文本、图片、音频、视频、文件。
OpenAiChatProvider 的 capabilities() 返回:
supports_stream = truesupports_image_input = truesupports_audio_input = truesupports_video_input = false(实现已映射视频输入,但出于谨慎暂不对外宣称)supports_tools = truesupports_structured_output = truesupports_parallel_tool_calls = true
构造方式
#![allow(unused)] fn main() { use kotoba_llm::provider::openai_chat::OpenAiChatProvider; use kotoba_llm::http::reqwest::default_dyn_transport; let transport = default_dyn_transport()?; let provider = OpenAiChatProvider::new(transport, std::env::var("OPENAI_API_KEY")?) .with_base_url("https://api.openai.com") .with_organization("org_123") .with_project("proj_alpha") .with_default_model("gpt-4.1-mini"); }
- 未在
ChatRequest.options.model指定模型时会 fallback 到with_default_model,两者都缺失会抛出LLMError::Validation { message: "model is required for OpenAI Chat" }。 organization与project分别映射到OpenAI-Organization、OpenAI-Projectheader。base_url自动补全/v1/chat/completions,支持自定义代理。
请求映射
request.rs::build_openai_body 把 ChatRequest 映射到 Chat Completions 结构:
messages:逐条转换Message→role/name/content/tool_calls。工具结果只能出现在role = "tool"的消息中,且最多一个ToolResult,否则触发LLMError::Validation。content支持:- 文本 →
{ type: "text", text } - 图片:URL / Base64(自动拼
data:mime;base64,...)/file_id - 音频:
input_audio.data/format - 视频:
input_video.source/format - 文件:
file.file_id - 自定义数据:直接嵌入 JSON
- 文本 →
- 采样参数:
temperature、top_p、max_output_tokens(映射为max_tokens)、presence_penalty、frequency_penalty。 - 工具:仅允许
ToolKind::Function;其他种类直接报LLMError::Validation。工具定义含name、description、parameters。 tool_choice:Auto/Any/None→ 字符串,Tool { name }→ function 对象,Custom完全透传。response_format:Text/JsonObject/JsonSchema/Custom全量支持,分别映射到 OpenAI 的response_format。reasoning:ReasoningOptions转换为reasoning_effort、max_reasoning_tokens及自定义字段。metadata:HashMap → JSON object。options.extra:逐键透传,可用于service_tier、logprobs等未统一的参数。stream:布尔值控制 SSE。
Streaming 与错误
stream_chat使用post_json_stream_with_headers建立 SSE,成功时返回ChatStream;- 若 HTTP 状态码非 2xx,会先收集流 body(
collect_stream_text),再调用parse_openai_error转换为LLMError; - 非流式路径会读取完整响应文本再解析
OpenAiChatResponse,解析失败统一返回LLMError::Provider { provider: "openai_chat", message: ... }。
常见校验
| 触发条件 | 错误 | 排查方式 |
|---|---|---|
ChatRequest 未声明模型且默认模型缺失 | LLMError::Validation | 在配置或请求层确保模型名称填写。 |
role=tool 的消息包含多个 ToolResult | LLMError::Validation { message: "tool role expects a single ToolResult content" } | 将多段工具结果拆分为多条 tool 消息或合并内容。 |
ToolCall.kind 非 Function | LLMError::Validation { message: "OpenAI only supports function tool calls" } | 统一使用 ToolKind::Function,或在业务层做条件判断。 |
extra 建议
ChatOptions.extra["service_tier"] = "default" | "scale":切换服务等级;ChatRequest.metadata:可放 trace id、租户信息,方便调试;- 若代理要求自定义 header,可在构造
HttpTransport时设置。
OpenAI Responses
适用场景
- 使用 OpenAI Responses API(
/v1/responses),统一处理文本、多模态、工具与结构化输出; - 需要 JSON Schema、
include、previous_response_id等 Responses 专属参数; - 希望在一个请求中组合 function、文件检索、Web 搜索、Computer Use 等工具。
capabilities() 返回:
supports_stream = truesupports_image_input = truesupports_audio_input = falsesupports_video_input = falsesupports_tools = truesupports_structured_output = truesupports_parallel_tool_calls = true
构造方式
与 Chat Provider 一致,提供 new/with_base_url/with_organization/with_project/with_default_model。同样地,缺少模型会以 LLMError::Validation { message: "model is required for OpenAI Responses" } 结束。
请求映射
build_openai_responses_body 的关键点:
- 系统或开发者消息折叠为
instructions(以两个换行连接),其余消息进入input数组,元素类型为:{ "type": "message", "role": "user", "content": [ ... ] } - 输入消息仅允许文本/图像/音频/视频/文件/自定义 JSON;若出现
ToolCall或ToolResult,立即报LLMError::Validation。 - 图片转换逻辑与 Chat 相同:支持 URL、Base64(拼接为
data:URL)、文件 ID,并带detail。 - 音频 (
input_audio) 与视频 (input_video) 映射与 Chat 相同;代码中已实现,但CapabilityDescriptor仍标记为false,用于提醒上层谨慎开启。 - 采样参数:
temperature、top_p、max_output_tokens、parallel_tool_calls。 - 推理配置:
ReasoningOptions.effort映射到reasoning.effort;extra里的其他字段同样写入reasoning对象。 - 工具:
ToolKind::Function→{ type: "function", name, description, parameters, strict: true };ToolKind::FileSearch/WebSearch/ComputerUse→ 使用metadata中的键补全,缺省type分别为file_search、web_search_preview、computer_use_preview;ToolKind::Custom→ 直接透传config或构造{ type: name, name: tool.name }。
tool_choice语义与 Chat 相同,但 Responses 官方用"auto"/"required"/"none"。response_format→text.format:Text:{ format: { type: "text" } }JsonObject:{ format: { type: "json_object" } }JsonSchema:自动补上name = "response"和schemaCustom:直接把对象作为text
metadata与options.extra直接写入顶层,方便控制include,service_tier,user,previous_response_id等参数。stream=true/false控制 SSE。
Streaming 与错误
逻辑与 Chat Provider 相同:
- 失败时收集 SSE 文本,调用
parse_openai_responses_error生成LLMError; - 非流式路径解析
OpenAiResponsesResponse,JSON 解析失败同样走LLMError::Provider。
工具与结构化输出注意事项
| 功能 | 细节 |
|---|---|
Function 工具 strict | 代码默认写入 strict: true,若需要关闭可在 ToolDefinition.metadata 中加 {"strict": false},映射逻辑会覆盖默认值。 |
| 非 function 工具 | 依赖 metadata 补齐字段,例如搜索索引 ID;如果 metadata 为空,则仅提供最基础的 type。 |
| 多工具并行 | ChatOptions.parallel_tool_calls = Some(false) 会让 tool_choice 中的 disable_parallel_tool_use = true(在 Responses 默认 true),从而限制模型同一轮内只触发一个工具。 |
| JSON 输出 | 推荐显式设置 response_format = JsonObject/JsonSchema,否则 Responses 仍可能返回富文本。 |
常见校验
- 输入数组为空:因为系统/开发者消息会被折叠,若没有 user/assistant 内容会导致
input缺失,但当前实现允许空输入;建议业务层确保至少有一条用户消息。 ToolCall直接放在Message.content中:convert_input_message会立即报错,请将工具相关信息写入ToolDefinition并交由模型触发。- 缺失
max_output_tokens:Responses API 允许缺省,代码也不会强制要求;如需硬性限制,请在业务层添加校验。
配置与 extra
extra.organization、extra.project设置共享 header;ChatOptions.extra中可以放include、service_tier、previous_response_id等 Responses 特有字段;ChatRequest.metadata建议写入 trace id、用户、上下文,用于后续排查。
Anthropic Messages
适用场景
- 对接 Claude 3.x Messages API,既要兼容文本也要兼容 base64 图像输入;
- 需要
thinking/reasoning 控制、并行工具开关; - 需要在请求头中注入
anthropic-version与anthropic-beta。
capabilities() 返回:
supports_stream = truesupports_image_input = true(仅 base64 来源)supports_audio_input = falsesupports_video_input = falsesupports_tools = truesupports_structured_output = falsesupports_parallel_tool_calls = true
构造方式
#![allow(unused)] fn main() { use kotoba_llm::provider::anthropic_messages::AnthropicMessagesProvider; use kotoba_llm::http::reqwest::default_dyn_transport; let provider = AnthropicMessagesProvider::new(default_dyn_transport()?, std::env::var("ANTHROPIC_KEY")?) .with_base_url("https://api.anthropic.com") .with_version("2023-06-01") .with_beta("prompt-caching,claude-3-opus-preview") .with_default_model("claude-3-5-sonnet-20241022"); }
- 版本号缺省为
2023-06-01,可通过extra.version覆盖; with_beta接受以逗号连接的标记;- 缺少模型会抛出
LLMError::Validation。
请求映射
build_anthropic_body 的特点:
system与developer角色的文本被折叠为system字符串(使用两个换行连接),其余消息进入messages数组,只允许user/assistant两种角色。- 要求至少存在一条
user/assistant消息,否则报 “Anthropic Messages request requires at least one user/assistant message”。 ChatOptions.max_output_tokens必填,对应max_tokens;缺失会直接报错。- 采样:
temperature、top_p。 reasoning:- 如果
ReasoningOptions.extra中包含thinking,将其完整透传; - 否则,当
budget_tokens存在时生成{ "type": "enabled", "budget_tokens": ... }并附加其余 extra; - 未提供
budget_tokens时默认不启用 thinking。
- 如果
- 工具:
ToolKind::Function→{ type: "custom", name, description, input_schema };ToolKind::Custom→ 直接透传config,或在缺省时构造{ type: name, name: tool.name };- 其他种类(如
FileSearch)会触发LLMError::Validation。
tool_choice:Auto/Any/Tool会附带disable_parallel_tool_use = !parallel_tool_calls;ToolChoice::None表示完全不发送tool_choice字段。- 内容支持:
- 文本;
- 图像:仅 Base64 源(URL / 文件 ID 会返回
LLMError::UnsupportedFeature { feature: "image_source_non_base64" }); - 工具结果:
ToolResult会转成tool_resultblock,要求call_id(映射为tool_use_id)。 - 其他内容类型(音频/视频/文件/ToolCall)都会触发
LLMError::UnsupportedFeature,如需扩展可通过ContentPart::Data自定义 JSON。
metadata与options.extra均直接写入顶层。stream布尔值控制 SSE。
Streaming 与错误
- 非 2xx 响应会将 SSE 全量收集后交由
parse_anthropic_error,因此日志中能看到原始 JSON; - JSON 解析失败时返回
LLMError::Provider { provider: "anthropic_messages", ... }; LLMError::UnsupportedFeature/Validation出现在请求映射阶段,可在单元测试中提前捕获。
常见坑位
| 问题 | 触发代码路径 | 定位建议 |
|---|---|---|
忘记设置 max_output_tokens | build_anthropic_body | 在构造 ChatRequest 时显式填写,或在配置层为特定模型设置默认值。 |
| 使用非 base64 图像 | convert_content_part | 先把远程图片下载转为 Base64,再放入 ImageContent::Base64。 |
tool_result 缺少 call_id | convert_content_part | 执行真实工具后,务必把模型返回的 ToolCall.id 回写到 ToolResult.call_id。 |
未设置 parallel_tool_calls 即想禁用并行 | convert_tool_choice | 显式把 ChatOptions.parallel_tool_calls = Some(false),生成 disable_parallel_tool_use = true。 |
extra 建议
options.extra["stop_sequences"] = ["Observation:"]:控制 Claude 的停止标记;options.extra["metadata"]:已经由ChatRequest.metadata承担,保持语义清晰即可;- 若要启用自定义 thinking 结构,可直接把完整对象放在
ReasoningOptions.extra["thinking"],代码会跳过自动推导逻辑。
Google Gemini
适用场景
- 需要一次性发送文本、图片、音频、视频等多模态内容;
- 依赖 Gemini 的
functionDeclarations工具体系、toolConfig调度策略、generationConfig.response_schema; - 需要官方
:streamGenerateContent?alt=sse流式接口。
capabilities() 返回:
supports_stream = truesupports_image_input = truesupports_audio_input = truesupports_video_input = truesupports_tools = truesupports_structured_output = truesupports_parallel_tool_calls = true
构造方式
#![allow(unused)] fn main() { use kotoba_llm::provider::google_gemini::GoogleGeminiProvider; use kotoba_llm::http::reqwest::default_dyn_transport; let provider = GoogleGeminiProvider::new(default_dyn_transport()?, std::env::var("GEMINI_KEY")?) .with_base_url("https://generativelanguage.googleapis.com") .with_default_model("gemini-2.0-flash"); }
model仅影响 URL(/v1beta/models/{model}:generateContent),JSON 体不携带模型字段;normalize_model会自动在缺失前缀时补上models/;- 既支持非流式
generateContent,也支持流式streamGenerateContent?alt=sse。
请求映射
build_gemini_body 主要处理以下逻辑:
system/developer消息折叠为system_instruction,格式为{ role: "system", parts: [{ text }] };其余消息进入contents数组,并将role = assistant替换为 Gemini 的model。- 要求
contents非空,否则返回 “Gemini GenerateContent request requires at least one content message”。 generationConfig:temperature、top_p(映射为topP)、max_output_tokens(maxOutputTokens)、presence_penalty、frequency_penalty;response_format = JsonObject⇒response_mime_type = application/json;response_format = JsonSchema⇒ 同时写入response_schema;response_format = Custom⇒ 直接把自定义对象作为整个generationConfig,覆盖其他字段。
- 内容映射:
- 文本 ⇒
{ "text": ... } - 图片 ⇒
- Base64 ⇒
inlineData(含mimeType) - URL/FileId ⇒
fileData(mimeType默认为application/octet-stream)
- Base64 ⇒
- 音频/视频 ⇒ 依据
MediaSource生成inlineData或fileData,并保留mimeType - 文件 ⇒
fileData - 自定义 JSON ⇒ 原样传递
ToolCall/ToolResult不允许直接放入消息,违例会返回LLMError::Validation
- 文本 ⇒
- 工具:
ToolKind::Function⇒{ functionDeclarations: [{ name, description, parameters }] }ToolKind::Custom⇒ 直接使用config,缺省时生成{ type: name, name: tool.name }- 其他种类会触发
LLMError::Validation
tool_choice映射到toolConfig.functionCallingConfig:Auto⇒ 不生成配置,沿用默认自动策略;Any⇒{ mode: "any" }None⇒{ mode: "none" }Tool { name }⇒{ mode: "any", allowedFunctionNames: [name] }Custom⇒ 直接透传
metadata:HashMap 直接写入。options.extra(如safetySettings、cachedContent)也完整透传。
Streaming 与错误
- 流式接口使用
:streamGenerateContent?alt=sse,收到的 SSE 由create_stream解析为ChatChunk; - 若状态码非 2xx,会先通过
collect_stream_text拼接完整 body,再交给parse_gemini_error,确保错误信息中包含官方字段; - JSON 解析失败时同样返回
LLMError::Provider { provider: "google_gemini", ... }。
常见校验
| 问题 | 触发 | 解决 |
|---|---|---|
| 没有用户内容(只有 system) | contents.is_empty() | 确保至少推入一条 user/assistant 消息。 |
| 直接把 ToolCall/ToolResult 加入消息 | convert_content_part 返回 LLMError::Validation | 使用工具定义 + tool_choice,由模型触发调用。 |
想强制 JSON 输出却只设置 response_format = Text | 无自动 JSON | 将 ResponseFormat::JsonObject 或 JsonSchema 赋值,或在 options.extra 自行设置 generationConfig.response_mime_type。 |
extra 建议
options.extra["safetySettings"]:传入 Gemini 官方的安全策略数组;options.extra["cachedContent"]:绑定到已缓存的上下文;metadata可放请求 ID、地理信息等,方便后续分析;- 若需要细粒度控制工具调度,可直接把完整的
toolConfig结构放在ToolChoice::Custom中,mapper 会原样传递。