分析对象:Rust workspace 的 rust/crates/api(HTTP client + provider 抽象 + SSE/流式解析 + OAuth token 解析/加载入口),并对照 rust/crates/runtime 侧的 OAuth/会话运行时接口(见 result/20.md)。


1. 目标:把“不同供应商”收敛成同一套调用与流式消费方式

一个成熟的 API client 抽象,需要同时满足:

  • 多提供商:不同 base URL、认证方式、payload 形状,但上层调用方式尽量一致。
  • OAuth:不仅支持 API key,还要支持 bearer token(含 refresh/过期处理)。
  • 流式响应:统一把 SSE/streaming 的碎片事件解析成结构化事件流,给 runtime loop 消费。

crates/api 的做法是:把“供应商差异”封进 Provider trait 与 provider 实现,把“调用入口”封进 ProviderClient,把“流式消费”封进 MessageStreamStreamEvent


2. 顶层 API 面:api::lib 的 re-export 设计

api/src/lib.rs 把核心类型统一 re-export,形成对上层友好的入口:

// 1:23:rust/crates/api/src/lib.rs
mod client;
mod error;
mod providers;
mod sse;
mod types;

pub use client::{
    oauth_token_is_expired, read_base_url, read_xai_base_url, resolve_saved_oauth_token,
    resolve_startup_auth_source, MessageStream, OAuthTokenSet, ProviderClient,
};
pub use error::ApiError;
pub use providers::claw_provider::{AuthSource, ClawApiClient, ClawApiClient as ApiClient};
pub use providers::openai_compat::{OpenAiCompatClient, OpenAiCompatConfig};
pub use providers::{ detect_provider_kind, resolve_model_alias, ProviderKind, ... };
pub use sse::{parse_frame, SseParser};
pub use types::{ MessageRequest, MessageResponse, StreamEvent, ToolDefinition, ToolChoice, ... };

学习点:上层(例如 CLI、runtime loop)无需了解 provider 文件布局;只依赖 api crate 的公开符号即可。这也是“统一接口”的第一步:统一 import 面


3. Provider 抽象:Provider trait + ProviderKind + 模型注册表

3.1 Provider trait:统一 send 与 stream

// 12:24:rust/crates/api/src/providers/mod.rs
pub trait Provider {
    type Stream;

    fn send_message<'a>(
        &'a self,
        request: &'a MessageRequest,
    ) -> ProviderFuture<'a, MessageResponse>;

    fn stream_message<'a>(
        &'a self,
        request: &'a MessageRequest,
    ) -> ProviderFuture<'a, Self::Stream>;
}

设计含义

  • send_message 固定返回 MessageResponse(统一响应结构)。
  • stream_message 返回 provider 自己的 stream 类型(但会被上层再封装为统一 MessageStream,见下)。

3.2 Provider 检测:模型名优先,其次环境探测

providers/mod.rs 维护一个 MODEL_REGISTRY(alias → ProviderMetadata),并提供:

  • resolve_model_alias(model) -> String
  • detect_provider_kind(model) -> ProviderKind
// 41:112:rust/crates/api/src/providers/mod.rs
const MODEL_REGISTRY: &[(&str, ProviderMetadata)] = &[
  ("opus", ProviderMetadata { provider: ProviderKind::ClawApi, auth_env: "ANTHROPIC_API_KEY", ... }),
  ("grok", ProviderMetadata { provider: ProviderKind::Xai, auth_env: "XAI_API_KEY", ... }),
  ...
];
// 187:202:rust/crates/api/src/providers/mod.rs
pub fn detect_provider_kind(model: &str) -> ProviderKind {
    if let Some(metadata) = metadata_for_model(model) {
        return metadata.provider;
    }
    if claw_provider::has_auth_from_env_or_saved().unwrap_or(false) {
        return ProviderKind::ClawApi;
    }
    if openai_compat::has_api_key("OPENAI_API_KEY") {
        return ProviderKind::OpenAi;
    }
    if openai_compat::has_api_key("XAI_API_KEY") {
        return ProviderKind::Xai;
    }
    ProviderKind::ClawApi
}

学习点:统一入口的关键是“先确定去哪家”。这里把路由策略写死在库里:模型名映射优先;否则用环境变量推断。


4. 统一客户端入口:ProviderClient(多提供商的单一 façade)

api/src/client.rsProviderClient enum 把多个 provider 的构造与调用收敛为一个类型:

// 21:50:rust/crates/api/src/client.rs
pub enum ProviderClient {
    ClawApi(ClawApiClient),
    Xai(OpenAiCompatClient),
    OpenAi(OpenAiCompatClient),
}

pub fn from_model_with_default_auth(model: &str, default_auth: Option<AuthSource>) -> Result<Self, ApiError> {
    let resolved_model = providers::resolve_model_alias(model);
    match providers::detect_provider_kind(&resolved_model) {
        ProviderKind::ClawApi => Ok(Self::ClawApi(match default_auth {
            Some(auth) => ClawApiClient::from_auth(auth),
            None => ClawApiClient::from_env()?,
        })),
        ProviderKind::Xai => Ok(Self::Xai(OpenAiCompatClient::from_env(OpenAiCompatConfig::xai())?)),
        ProviderKind::OpenAi => Ok(Self::OpenAi(OpenAiCompatClient::from_env(OpenAiCompatConfig::openai())?)),
    }
}

调用层同样被统一:

// 61:83:rust/crates/api/src/client.rs
pub async fn send_message(&self, request: &MessageRequest) -> Result<MessageResponse, ApiError> { ... }

pub async fn stream_message(&self, request: &MessageRequest) -> Result<MessageStream, ApiError> {
    match self {
        Self::ClawApi(client) => stream_via_provider(client, request).await.map(MessageStream::ClawApi),
        Self::Xai(client) | Self::OpenAi(client) => stream_via_provider(client, request).await.map(MessageStream::OpenAiCompat),
    }
}

学习点:上层 runtime loop 只要持有 ProviderClient,就能在不关心具体 provider 的情况下 send_message / stream_message


5. 流式响应统一:MessageStream + StreamEvent(消费侧稳定)

client.rs 把不同 provider 的 stream 封成一个 MessageStream enum,并提供统一消费接口:

// 86:107:rust/crates/api/src/client.rs
pub enum MessageStream {
    ClawApi(claw_provider::MessageStream),
    OpenAiCompat(openai_compat::MessageStream),
}

impl MessageStream {
    pub fn request_id(&self) -> Option<&str> { ... }

    pub async fn next_event(&mut self) -> Result<Option<StreamEvent>, ApiError> {
        match self {
            Self::ClawApi(stream) => stream.next_event().await,
            Self::OpenAiCompat(stream) => stream.next_event().await,
        }
    }
}

同时,事件类型 StreamEvent 与相关 message/tool delta/start/stop 结构体在 types.rs 中统一定义并 re-export(见 api/src/lib.rs),从而让上层“只消费统一事件流”。

工程含义:多 provider 的差异应该被压在“解析层”,上层只看到一致的 event 语义(text delta、tool use、message stop、usage 等)。


6. OAuth 与认证:AuthSource + OAuthTokenSet(把多种凭证统一成 header 注入)

ClawApiClient(Anthropic/Claw API 形态)为例,认证被抽象为 AuthSource

// 24:33:rust/crates/api/src/providers/claw_provider.rs
pub enum AuthSource {
    None,
    ApiKey(String),
    BearerToken(String),
    ApiKeyAndBearer { api_key: String, bearer_token: String },
}

并提供统一的 header 注入:

// 82:90:rust/crates/api/src/providers/claw_provider.rs
pub fn apply(&self, mut request_builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
    if let Some(api_key) = self.api_key() {
        request_builder = request_builder.header("x-api-key", api_key);
    }
    if let Some(token) = self.bearer_token() {
        request_builder = request_builder.bearer_auth(token);
    }
    request_builder
}

OAuth token 集合被建模为 OAuthTokenSet,并可转成 AuthSource::BearerToken

// 93:106:rust/crates/api/src/providers/claw_provider.rs
pub struct OAuthTokenSet {
    pub access_token: String,
    pub refresh_token: Option<String>,
    pub expires_at: Option<u64>,
    #[serde(default)]
    pub scopes: Vec<String>,
}

impl From<OAuthTokenSet> for AuthSource {
    fn from(value: OAuthTokenSet) -> Self {
        Self::BearerToken(value.access_token)
    }
}

连接到 runtime:文件开头直接使用 runtime crate 的 OAuth 凭证读写与 refresh/exchange request 类型:

// 4:7:rust/crates/api/src/providers/claw_provider.rs
use runtime::{
    load_oauth_credentials, save_oauth_credentials, OAuthConfig, OAuthRefreshRequest,
    OAuthTokenExchangeRequest,
};

这体现了一个关键分工:

  • runtime 提供 OAuth “协议与持久化”能力(PKCE、credential 文件等,见 result/20.md)。
  • api 把 token 变成“可以打到 HTTP header 上的 AuthSource”,并用于请求重试与流式解析。

7. 错误模型:ApiError 既面向人类也面向重试策略

ApiError 既区分 MissingCredentials/ExpiredOAuthToken/HTTP/JSON,也提供 is_retryable()

// 5:33:rust/crates/api/src/error.rs
pub enum ApiError {
    MissingCredentials { provider: &'static str, env_vars: &'static [&'static str] },
    ExpiredOAuthToken,
    Auth(String),
    Http(reqwest::Error),
    ...
    Api { status: reqwest::StatusCode, body: String, retryable: bool, ... },
    RetriesExhausted { attempts: u32, last_error: Box<ApiError> },
    InvalidSseFrame(&'static str),
    ...
}
// 44:59:rust/crates/api/src/error.rs
pub fn is_retryable(&self) -> bool {
    match self {
        Self::Http(error) => error.is_connect() || error.is_timeout() || error.is_request(),
        Self::Api { retryable, .. } => *retryable,
        Self::RetriesExhausted { last_error, .. } => last_error.is_retryable(),
        _ => false,
    }
}

学习点:统一接口不仅要统一“成功返回”,也要统一“失败语义”。可重试性作为方法暴露出来,能让上层 runtime loop 做策略化处理,而不是散落 if-else。


8. OpenAI-compat:用适配器把不同协议翻译成同一事件/响应结构

providers/openai_compat.rs(用于 OpenAI 与 xAI/Grok 兼容接口)实现 Provider trait,并在内部把 chat-completions 的 streaming/tool_calls 翻译成 MessageResponse/StreamEvent 体系(从 grep 结果可见它显式设置 stream: true 并解析 SSE)。

学习点:多提供商统一的主成本在“协议差异翻译”;该仓库把它封装在 provider 实现内部,使得上层 ProviderClient 不变。


9. 小结:统一接口的“最小形状”

crates/api 的现状,可以把“统一接口长什么样”总结成三件套:

  1. Provider trait:统一 send_message / stream_message 的抽象点。
  2. ProviderClient façade:统一“从 model/环境选择 provider + 构造 client + 发请求”的入口。
  3. MessageStream + StreamEvent:统一 streaming 消费语义,让 runtime loop 只关心事件而不关心 SSE 细节。

OAuth/ApiKey/多 base_url 则通过 AuthSource、env metadata、以及 provider 内部策略统一承载。整体形状非常适合被 runtime(ConversationRuntime)消费,形成“系统语言的 definitive runtime”上层闭环(见 result/20.md)。


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐