claw-code 源码分析:API Client 抽象——多提供商、OAuth、流式响应的统一接口长什么样?
分析对象: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的做法是:把“供应商差异”封进Providertrait 与 provider 实现,把“调用入口”封进ProviderClient,把“流式消费”封进MessageStream与StreamEvent。
2. 顶层 API 面:api::lib的 re-export 设计
api/src/lib.rs把核心类型统一 re-export,形成对上层友好的入口:
// 1:23:rust/crates/api/src/lib.rsmodclient;moderror;modproviders;modsse;modtypes;pubuseclient::{oauth_token_is_expired,read_base_url,read_xai_base_url,resolve_saved_oauth_token,resolve_startup_auth_source,MessageStream,OAuthTokenSet,ProviderClient,};pubuseerror::ApiError;pubuseproviders::claw_provider::{AuthSource,ClawApiClient,ClawApiClientasApiClient};pubuseproviders::openai_compat::{OpenAiCompatClient,OpenAiCompatConfig};pubuseproviders::{detect_provider_kind,resolve_model_alias,ProviderKind,...};pubusesse::{parse_frame,SseParser};pubusetypes::{MessageRequest,MessageResponse,StreamEvent,ToolDefinition,ToolChoice,...};学习点:上层(例如 CLI、runtime loop)无需了解 provider 文件布局;只依赖apicrate 的公开符号即可。这也是“统一接口”的第一步:统一 import 面。
3. Provider 抽象:Providertrait +ProviderKind+ 模型注册表
3.1Providertrait:统一 send 与 stream
// 12:24:rust/crates/api/src/providers/mod.rspubtraitProvider{typeStream;fnsend_message<'a>(&'aself,request:&'aMessageRequest,)->ProviderFuture<'a,MessageResponse>;fnstream_message<'a>(&'aself,request:&'aMessageRequest,)->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) -> Stringdetect_provider_kind(model) -> ProviderKind
// 41:112:rust/crates/api/src/providers/mod.rsconstMODEL_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.rspubfndetect_provider_kind(model:&str)->ProviderKind{ifletSome(metadata)=metadata_for_model(model){returnmetadata.provider;}ifclaw_provider::has_auth_from_env_or_saved().unwrap_or(false){returnProviderKind::ClawApi;}ifopenai_compat::has_api_key("OPENAI_API_KEY"){returnProviderKind::OpenAi;}ifopenai_compat::has_api_key("XAI_API_KEY"){returnProviderKind::Xai;}ProviderKind::ClawApi}学习点:统一入口的关键是“先确定去哪家”。这里把路由策略写死在库里:模型名映射优先;否则用环境变量推断。
4. 统一客户端入口:ProviderClient(多提供商的单一 façade)
api/src/client.rs用ProviderClientenum 把多个 provider 的构造与调用收敛为一个类型:
// 21:50:rust/crates/api/src/client.rspubenumProviderClient{ClawApi(ClawApiClient),Xai(OpenAiCompatClient),OpenAi(OpenAiCompatClient),}pubfnfrom_model_with_default_auth(model:&str,default_auth:Option<AuthSource>)->Result<Self,ApiError>{letresolved_model=providers::resolve_model_alias(model);matchproviders::detect_provider_kind(&resolved_model){ProviderKind::ClawApi=>Ok(Self::ClawApi(matchdefault_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.rspubasyncfnsend_message(&self,request:&MessageRequest)->Result<MessageResponse,ApiError>{...}pubasyncfnstream_message(&self,request:&MessageRequest)->Result<MessageStream,ApiError>{matchself{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 封成一个MessageStreamenum,并提供统一消费接口:
// 86:107:rust/crates/api/src/client.rspubenumMessageStream{ClawApi(claw_provider::MessageStream),OpenAiCompat(openai_compat::MessageStream),}implMessageStream{pubfnrequest_id(&self)->Option<&str>{...}pubasyncfnnext_event(&mutself)->Result<Option<StreamEvent>,ApiError>{matchself{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.rspubenumAuthSource{None,ApiKey(String),BearerToken(String),ApiKeyAndBearer{api_key:String,bearer_token:String},}并提供统一的 header 注入:
// 82:90:rust/crates/api/src/providers/claw_provider.rspubfnapply(&self,mutrequest_builder:reqwest::RequestBuilder)->reqwest::RequestBuilder{ifletSome(api_key)=self.api_key(){request_builder=request_builder.header("x-api-key",api_key);}ifletSome(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.rspubstructOAuthTokenSet{pubaccess_token:String,pubrefresh_token:Option<String>,pubexpires_at:Option<u64>,#[serde(default)]pubscopes:Vec<String>,}implFrom<OAuthTokenSet>forAuthSource{fnfrom(value:OAuthTokenSet)->Self{Self::BearerToken(value.access_token)}}连接到 runtime:文件开头直接使用runtimecrate 的 OAuth 凭证读写与 refresh/exchange request 类型:
// 4:7:rust/crates/api/src/providers/claw_provider.rsuseruntime::{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.rspubenumApiError{MissingCredentials{provider:&'staticstr,env_vars:&'static[&'staticstr]},ExpiredOAuthToken,Auth(String),Http(reqwest::Error),...Api{status:reqwest::StatusCode,body:String,retryable:bool,...},RetriesExhausted{attempts:u32,last_error:Box<ApiError>},InvalidSseFrame(&'staticstr),...}// 44:59:rust/crates/api/src/error.rspubfnis_retryable(&self)->bool{matchself{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 兼容接口)实现Providertrait,并在内部把 chat-completions 的 streaming/tool_calls 翻译成MessageResponse/StreamEvent体系(从 grep 结果可见它显式设置stream: true并解析 SSE)。
学习点:多提供商统一的主成本在“协议差异翻译”;该仓库把它封装在 provider 实现内部,使得上层ProviderClient不变。
9. 小结:统一接口的“最小形状”
从crates/api的现状,可以把“统一接口长什么样”总结成三件套:
- Provider trait:统一
send_message/stream_message的抽象点。 - ProviderClient façade:统一“从 model/环境选择 provider + 构造 client + 发请求”的入口。
- MessageStream + StreamEvent:统一 streaming 消费语义,让 runtime loop 只关心事件而不关心 SSE 细节。
OAuth/ApiKey/多 base_url 则通过AuthSource、env metadata、以及 provider 内部策略统一承载。整体形状非常适合被 runtime(ConversationRuntime)消费,形成“系统语言的 definitive runtime”上层闭环(见result/20.md)。
