概览

kotoba 是一个 Rust 编写的“统一 LLM 调用层”,通过一套共享的 MessageContentPartChatRequestChatResponse 等类型,将 OpenAI Chat、OpenAI Responses、Anthropic Messages、Google Gemini 等主流厂商封装进统一接口。仓库暴露的 LLMClientLLMProviderHttpTransport 抽象允许你:

  • 在同一进程内注册多个 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定义 RoleMessageContentPartChatRequestChatResponseChatChunk 的全量数据结构,覆盖文本、多模态、工具、推理与流式事件。
src/provider暴露 LLMProvider trait 与具体供应商实现,负责将统一模型映射为厂商 API 请求并解析响应。
src/client提供 LLMClientLLMClientBuilder,路由 handle → Provider,支持能力查询及工具/流式筛选。
src/configModelConfig/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 枚举;
  • 音频 / 视频:AudioContentVideoContent 结合 MediaSource::Inline/FileId/Url
  • 文件引用、原始 JSON 数据;
  • 工具调用 (ToolCall) 与工具结果 (ToolResult),二者在不同 Provider 中被各自的 request mapper 处理。

ChatRequest 附带 ChatOptions(温度、top_pmax_output_tokens、penalty、parallel_tool_callsReasoningOptionsextra),并行工具策略由 ToolChoice 决定,输出格式通过 ResponseFormat 声明(文本 / JSON / JSON Schema / 自定义)。

ChatResponse 统一封装 OutputItem(消息、工具、工具结果、推理文本、自定义 payload)、TokenUsageFinishReasonProviderMetadata。流式场景使用 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_streamsupports_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 暴露以下字段:handleprovidercredentialdefault_modelbase_urlextraProviderKind 枚举包含四个当前实现。Credential 支持 ApiKey(可自定义 header)、BearerServiceAccount(仅做校验,当前 Provider 均不接受)、Nonebuild_client_from_configs 按序构造 Provider 并注册 handle,遇到缺少凭证、重复 handle、或 Service Account 等不被支持的 credential 时抛出 LLMError::Auth/Validation

HTTP 抽象

HttpTransport 提供 send(HttpRequest) -> HttpResponsesend_stream(HttpRequest) -> HttpStreamResponse。默认的 ReqwestTransport 实现在 http/reqwest.rs,但调用方可注入自定义实现以满足:

  • 统一代理/超时/重试策略;
  • 在测试中返回固定 JSON 或模拟 SSE。

post_json_with_headerspost_json_stream_with_headers 小工具隐藏了重复的 header 注入逻辑,使 Provider 实现专注于业务映射。

错误处理

所有错误通过 LLMError 传播:

  • 网络层:Transport
  • 鉴权:Auth
  • 速率限制:RateLimit(附带 retry_after);
  • Token/上下文溢出:TokenLimitExceeded(保留原始消息);
  • 模型缺失或不可用:ModelNotFound
  • 运行时请求校验:Validation
  • 配置阶段错误:InvalidConfig
  • 用户主动取消:Aborted
  • SSE 早退:StreamClosed(含原始错误信息);
  • 能力不支持:UnsupportedFeature
  • 厂商返回:Provider
  • 预留占位:NotImplementedUnknown

在 Provider 内可以直接使用 LLMError::transport()LLMError::provider() 保持信息格式一致,便于上层日志与重试策略集中处理。

客户端与配置装载

config::build_client_from_configs 让你在启动阶段就把所有 Provider 注册好,避免在业务代码里散落大量构造逻辑。本章说明配置字段及各 Provider 支持的额外键。

ModelConfig 字段

字段说明
handle注册到 LLMClient 的唯一名称,后续 client.chat(handle, ..) 通过它路由。重复 handle 会立即触发 LLMError::InvalidConfig
providerProviderKind 枚举,当前支持 OpenAiChatOpenAiResponsesAnthropicMessagesGoogleGemini
credentialCredential::ApiKey { header, key }Credential::Bearer { token }Credential::ServiceAccount { json }Credential::None。除 ServiceAccount/None 外都会映射到对应 Provider;不满足条件时返回 LLMError::Auth
default_modelChatRequest.options.model 为空时的兜底模型。绝大多数 Provider 都在请求阶段要求模型,缺失会报 LLMError::Validation
base_url可选的自定义地址,便于本地代理或企业网关。构造时会调用 Provider 的 with_base_url
extraHashMap<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 字段约定

Providerextra作用
OpenAiChat / OpenAiResponsesorganizationproject分别映射到 OpenAI-OrganizationOpenAI-Project header。
AnthropicMessagesversionbeta映射到 anthropic-versionanthropic-beta header,用逗号分隔多个 beta。
GoogleGemini(暂无默认键)可以自定义,例如 safetySettingscachedContent,直接透传到请求 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_modelProvider 构造成功,但在运行时调用 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 字段

字段类型说明
urlOption<String>覆盖请求的完整 URL
bodyOption<Value>深度合并到请求 body 中的 JSON 对象
headersOption<HashMap<String, Option<String>>>添加/覆盖 HTTP headers。值为 None 时删除该 header
remove_fieldsOption<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()]),
};
}

补丁应用顺序

补丁按以下顺序应用到请求中:

  1. URL 替换:如果 patch.url 存在,覆盖原始 URL
  2. Body 深度合并:将 patch.body 递归合并到请求 body 中
  3. Headers 修改:添加/覆盖/删除 HTTP headers
  4. 字段移除:从 body 中删除指定路径的字段

注意事项

  • 深度合并body 中的对象会递归合并,不会简单覆盖整个对象
  • 类型替换:如果 patch 中的值类型与原值不同,会完全替换
  • 路径语法remove_fields 使用点分路径,例如 metadata.user_idmessages.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_headerspost_json_stream_with_headers 帮助 Provider 构造 POST + JSON 请求。

默认实现

ReqwestTransportsrc/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 统一设置,推荐:

  1. 自定义 reqwest::Client,在 builder 中配置超时与代理,然后传入 ReqwestTransport::new(client)
  2. 或者实现一个包装器,在调用 inner.send() 前设置 request.timeout、注入 trace header、记录 metrics。

这样既不会污染 Provider 的业务逻辑,也让网络策略集中可控。

Provider 指南

本章聚焦四个内置 Provider 的特性、差异与使用场景。无论是直接手动构造 Provider,还是通过 ModelConfig 装载,都可以参考以下维度挑选:

能力矩阵

ProviderStreaming图像输入音频输入视频输入工具调用结构化输出并行工具
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;
  • 希望在请求体中同时发送文本、图片、音频、视频、文件。

OpenAiChatProvidercapabilities() 返回:

  • supports_stream = true
  • supports_image_input = true
  • supports_audio_input = true
  • supports_video_input = false(实现已映射视频输入,但出于谨慎暂不对外宣称)
  • supports_tools = true
  • supports_structured_output = true
  • supports_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" }
  • organizationproject 分别映射到 OpenAI-OrganizationOpenAI-Project header。
  • base_url 自动补全 /v1/chat/completions,支持自定义代理。

请求映射

request.rs::build_openai_bodyChatRequest 映射到 Chat Completions 结构:

  1. messages:逐条转换 Messagerole/name/content/tool_calls。工具结果只能出现在 role = "tool" 的消息中,且最多一个 ToolResult,否则触发 LLMError::Validation
  2. content 支持:
    • 文本 → { type: "text", text }
    • 图片:URL / Base64(自动拼 data:mime;base64,...)/ file_id
    • 音频:input_audio.data/format
    • 视频:input_video.source/format
    • 文件:file.file_id
    • 自定义数据:直接嵌入 JSON
  3. 采样参数:temperaturetop_pmax_output_tokens(映射为 max_tokens)、presence_penaltyfrequency_penalty
  4. 工具:仅允许 ToolKind::Function;其他种类直接报 LLMError::Validation。工具定义含 namedescriptionparameters
  5. tool_choiceAuto/Any/None → 字符串,Tool { name } → function 对象,Custom 完全透传。
  6. response_formatText/JsonObject/JsonSchema/Custom 全量支持,分别映射到 OpenAI 的 response_format
  7. reasoningReasoningOptions 转换为 reasoning_effortmax_reasoning_tokens 及自定义字段。
  8. metadata:HashMap → JSON object。
  9. options.extra:逐键透传,可用于 service_tierlogprobs 等未统一的参数。
  10. 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 的消息包含多个 ToolResultLLMError::Validation { message: "tool role expects a single ToolResult content" }将多段工具结果拆分为多条 tool 消息或合并内容。
ToolCall.kindFunctionLLMError::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、includeprevious_response_id 等 Responses 专属参数;
  • 希望在一个请求中组合 function、文件检索、Web 搜索、Computer Use 等工具。

capabilities() 返回:

  • supports_stream = true
  • supports_image_input = true
  • supports_audio_input = false
  • supports_video_input = false
  • supports_tools = true
  • supports_structured_output = true
  • supports_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 的关键点:

  1. 系统或开发者消息折叠为 instructions(以两个换行连接),其余消息进入 input 数组,元素类型为:
    { "type": "message", "role": "user", "content": [ ... ] }
    
  2. 输入消息仅允许文本/图像/音频/视频/文件/自定义 JSON;若出现 ToolCallToolResult,立即报 LLMError::Validation
  3. 图片转换逻辑与 Chat 相同:支持 URL、Base64(拼接为 data: URL)、文件 ID,并带 detail
  4. 音频 (input_audio) 与视频 (input_video) 映射与 Chat 相同;代码中已实现,但 CapabilityDescriptor 仍标记为 false,用于提醒上层谨慎开启。
  5. 采样参数:temperaturetop_pmax_output_tokensparallel_tool_calls
  6. 推理配置:ReasoningOptions.effort 映射到 reasoning.effortextra 里的其他字段同样写入 reasoning 对象。
  7. 工具:
    • ToolKind::Function{ type: "function", name, description, parameters, strict: true }
    • ToolKind::FileSearch/WebSearch/ComputerUse → 使用 metadata 中的键补全,缺省 type 分别为 file_searchweb_search_previewcomputer_use_preview
    • ToolKind::Custom → 直接透传 config 或构造 { type: name, name: tool.name }
  8. tool_choice 语义与 Chat 相同,但 Responses 官方用 "auto"/"required"/"none"
  9. response_formattext.format
    • Text{ format: { type: "text" } }
    • JsonObject{ format: { type: "json_object" } }
    • JsonSchema:自动补上 name = "response"schema
    • Custom:直接把对象作为 text
  10. metadataoptions.extra 直接写入顶层,方便控制 include, service_tier, user, previous_response_id 等参数。
  11. 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.organizationextra.project 设置共享 header;
  • ChatOptions.extra 中可以放 includeservice_tierprevious_response_id 等 Responses 特有字段;
  • ChatRequest.metadata 建议写入 trace id、用户、上下文,用于后续排查。

Anthropic Messages

适用场景

  • 对接 Claude 3.x Messages API,既要兼容文本也要兼容 base64 图像输入;
  • 需要 thinking/reasoning 控制、并行工具开关;
  • 需要在请求头中注入 anthropic-versionanthropic-beta

capabilities() 返回:

  • supports_stream = true
  • supports_image_input = true(仅 base64 来源)
  • supports_audio_input = false
  • supports_video_input = false
  • supports_tools = true
  • supports_structured_output = false
  • supports_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 的特点:

  1. systemdeveloper 角色的文本被折叠为 system 字符串(使用两个换行连接),其余消息进入 messages 数组,只允许 user/assistant 两种角色。
  2. 要求至少存在一条 user/assistant 消息,否则报 “Anthropic Messages request requires at least one user/assistant message”。
  3. ChatOptions.max_output_tokens 必填,对应 max_tokens;缺失会直接报错。
  4. 采样:temperaturetop_p
  5. reasoning
    • 如果 ReasoningOptions.extra 中包含 thinking,将其完整透传;
    • 否则,当 budget_tokens 存在时生成 { "type": "enabled", "budget_tokens": ... } 并附加其余 extra;
    • 未提供 budget_tokens 时默认不启用 thinking。
  6. 工具:
    • ToolKind::Function{ type: "custom", name, description, input_schema }
    • ToolKind::Custom → 直接透传 config,或在缺省时构造 { type: name, name: tool.name }
    • 其他种类(如 FileSearch)会触发 LLMError::Validation
  7. tool_choiceAuto/Any/Tool 会附带 disable_parallel_tool_use = !parallel_tool_callsToolChoice::None 表示完全不发送 tool_choice 字段。
  8. 内容支持:
    • 文本;
    • 图像: Base64 源(URL / 文件 ID 会返回 LLMError::UnsupportedFeature { feature: "image_source_non_base64" });
    • 工具结果:ToolResult 会转成 tool_result block,要求 call_id(映射为 tool_use_id)。
    • 其他内容类型(音频/视频/文件/ToolCall)都会触发 LLMError::UnsupportedFeature,如需扩展可通过 ContentPart::Data 自定义 JSON。
  9. metadataoptions.extra 均直接写入顶层。
  10. stream 布尔值控制 SSE。

Streaming 与错误

  • 非 2xx 响应会将 SSE 全量收集后交由 parse_anthropic_error,因此日志中能看到原始 JSON;
  • JSON 解析失败时返回 LLMError::Provider { provider: "anthropic_messages", ... }
  • LLMError::UnsupportedFeature / Validation 出现在请求映射阶段,可在单元测试中提前捕获。

常见坑位

问题触发代码路径定位建议
忘记设置 max_output_tokensbuild_anthropic_body在构造 ChatRequest 时显式填写,或在配置层为特定模型设置默认值。
使用非 base64 图像convert_content_part先把远程图片下载转为 Base64,再放入 ImageContent::Base64
tool_result 缺少 call_idconvert_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 = true
  • supports_image_input = true
  • supports_audio_input = true
  • supports_video_input = true
  • supports_tools = true
  • supports_structured_output = true
  • supports_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 主要处理以下逻辑:

  1. system/developer 消息折叠为 system_instruction,格式为 { role: "system", parts: [{ text }] };其余消息进入 contents 数组,并将 role = assistant 替换为 Gemini 的 model
  2. 要求 contents 非空,否则返回 “Gemini GenerateContent request requires at least one content message”。
  3. generationConfig
    • temperaturetop_p(映射为 topP)、max_output_tokensmaxOutputTokens)、presence_penaltyfrequency_penalty
    • response_format = JsonObjectresponse_mime_type = application/json
    • response_format = JsonSchema ⇒ 同时写入 response_schema
    • response_format = Custom ⇒ 直接把自定义对象作为整个 generationConfig,覆盖其他字段。
  4. 内容映射:
    • 文本 ⇒ { "text": ... }
    • 图片 ⇒
      • Base64 ⇒ inlineData(含 mimeType
      • URL/FileId ⇒ fileDatamimeType 默认为 application/octet-stream
    • 音频/视频 ⇒ 依据 MediaSource 生成 inlineDatafileData,并保留 mimeType
    • 文件 ⇒ fileData
    • 自定义 JSON ⇒ 原样传递
    • ToolCall/ToolResult 不允许直接放入消息,违例会返回 LLMError::Validation
  5. 工具:
    • ToolKind::Function{ functionDeclarations: [{ name, description, parameters }] }
    • ToolKind::Custom ⇒ 直接使用 config,缺省时生成 { type: name, name: tool.name }
    • 其他种类会触发 LLMError::Validation
  6. tool_choice 映射到 toolConfig.functionCallingConfig
    • Auto ⇒ 不生成配置,沿用默认自动策略;
    • Any{ mode: "any" }
    • None{ mode: "none" }
    • Tool { name }{ mode: "any", allowedFunctionNames: [name] }
    • Custom ⇒ 直接透传
  7. metadata:HashMap 直接写入。options.extra(如 safetySettingscachedContent)也完整透传。

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无自动 JSONResponseFormat::JsonObjectJsonSchema 赋值,或在 options.extra 自行设置 generationConfig.response_mime_type

extra 建议

  • options.extra["safetySettings"]:传入 Gemini 官方的安全策略数组;
  • options.extra["cachedContent"]:绑定到已缓存的上下文;
  • metadata 可放请求 ID、地理信息等,方便后续分析;
  • 若需要细粒度控制工具调度,可直接把完整的 toolConfig 结构放在 ToolChoice::Custom 中,mapper 会原样传递。