ChatGPT与API-Key认证接入
本文承接 ModelProviderInfo字段、模型请求构造 和 Backend Client统一访问。前两篇解释 provider 配置和 Responses 请求如何形成;后者解释业务 backend 如何消费认证。本文把焦点收窄到“凭据如何从登录状态走到一次模型请求”,不展开 AWS SigV4,也不把 Realtime 专用的 API key 提取逻辑当成普通 Responses 认证。
默认读者知道 Rust trait、Arc、异步 future 和 HTTP Header。读完后,应能从 CodexAuth 的变体判断默认 endpoint,定位 ModelProvider::api_auth_for_scope 的生效时机,并解释为什么刷新 token 可以被同一请求状态看到、切换账户却不能复用旧状态。
1. 先分三层
认证不是一个字段,而是三个 owner 的协作:CodexAuth 保存身份事实,ModelProvider 按 provider 配置解析有效凭据,AuthProvider 在发送前修改请求。Backend Client 只是其中一个 consumer;模型请求、模型目录和 WebSocket 也通过 codex-api 的同一抽象消费它。
这里的层次差异很重要:auth_mode() 是路由和 provider 默认值的输入,不等于“本次请求一定带哪一个 Header”;Header 的实际内容要等 AuthProvider::add_auth_headers 被调用才确定。
2. 认证变体
CodexAuth 同时容纳用户登录、外部托管 token、预先构造的 Header、Agent Identity、Personal Access Token 以及 Bedrock 专用 key。auth_mode() 会把 ChatgptAuthTokens 归一到 AuthMode::Chatgpt,而 api_auth_mode() 保留精确的存储来源;后者用于区分刷新能力和行为判断。
源码位置:codex-rs/login/src/auth/manager.rs :: CodexAuth、CodexAuth::auth_mode、CodexAuth::api_auth_mode
pub enum CodexAuth {
ApiKey(ApiKeyAuth),
Chatgpt(ChatgptAuth),
ChatgptAuthTokens(ChatgptAuthTokens),
Headers(AuthHeaders),
AgentIdentity(AgentIdentityAuth),
PersonalAccessToken(PersonalAccessTokenAuth),
BedrockApiKey(BedrockApiKeyAuth),
}
pub fn auth_mode(&self) -> AuthMode {
match self {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => AuthMode::Chatgpt,
Self::Headers(_) => AuthMode::Headers,
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
}
}api_key() 只对 ApiKey 变体返回值;ChatGPT access token 通过 get_token() 从当前 token data 读取,Agent Identity 则明确拒绝暴露 bearer token。这防止调用方把签名型身份误当作普通 API key。
3. Provider选路
ModelProviderInfo::to_api_provider 根据认证模式选择默认 base URL:ChatGPT、Headers、Agent Identity 和 Personal Access Token 走 ChatGPT Codex endpoint;API key 使用 https://api.openai.com/v1。显式 base_url 会覆盖默认值。这个判断发生在 ModelProvider::api_provider(),早于请求构造。
源码位置:codex-rs/model-provider-info/src/lib.rs :: ModelProviderInfo::to_api_provider
let default_base_url = if matches!(
auth_mode,
Some(AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens | AuthMode::Headers
| AuthMode::AgentIdentity | AuthMode::PersonalAccessToken)
) {
CHATGPT_CODEX_BASE_URL
} else {
"https://api.openai.com/v1"
};
let base_url = self.base_url.clone().unwrap_or_else(|| default_base_url.to_string());但 endpoint 选路和认证 Header 仍是两件事。provider_uses_first_party_auth_path 只有在 requires_openai_auth 为真且没有 env_key、自定义 bearer、command auth 或 AWS 配置时才成立;否则 scoped Agent Identity bootstrap 不会介入。
4. Header注入
普通 bearer 认证由 BearerAuthProvider 所有。它只在请求发送前写入 Authorization;若有账户 ID 或 FedRAMP 标记,再写入路由 Header。缺少 token 时不会凭空生成空的 Authorization。
源码位置:codex-rs/model-provider/src/bearer_auth_provider.rs :: BearerAuthProvider::add_auth_headers
fn add_auth_headers(&self, headers: &mut HeaderMap) {
if let Some(token) = self.token.as_ref()
&& let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}"))
{
let _ = headers.insert(http::header::AUTHORIZATION, header);
}
if let Some(account_id) = self.account_id.as_ref()
&& let Ok(header) = HeaderValue::from_str(account_id)
{
let _ = headers.insert("ChatGPT-Account-ID", header);
}
if self.is_fedramp_account {
let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true"));
}
}Agent Identity 走另一条实现:authorization_header_for_agent_task 生成 AgentAssertion,但账户 ID 与 FedRAMP Header 仍保留。这说明“认证方案”与“账户路由信息”是正交的,不能只检查 Authorization 就推断完整请求身份。
5. 请求消费
模型请求在 core::client::current_client_setup 每次建立当前 client setup 时重新读取 provider、auth 和 scoped auth。这样 token refresh 可以在下一次 setup 被看到;setup 本身只持有本次请求要用的 SharedAuthProvider,不会把旧 token 写回全局登录状态。
源码位置:codex-rs/core/src/client.rs :: current_client_setup
async fn current_client_setup(&self) -> Result<CurrentClientSetup> {
let auth = self.state.provider.auth().await;
let api_provider = self.state.provider.api_provider().await?;
let resolved_auth = self.state.provider.api_auth_for_scope(ProviderAuthScope {
agent_identity_policy: self.agent_identity_policy,
session_source: self.state.session_source.clone(),
agent_identity_session_fallback: self.state.agent_identity_session_fallback.clone(),
}).await?;
Ok(CurrentClientSetup {
auth,
api_provider,
api_auth: resolved_auth.auth,
agent_identity_telemetry: resolved_auth.agent_identity_telemetry,
})
}Backend Client 的消费点则是 headers():先调用 auth provider,再叠加 client 自己保存的账户/FedRAMP 路由字段,最后由 RouteAwareRequestBuilder 发出请求。对于同一个 Header,后写入的 client 字段会覆盖 provider 先写入的值,因此调用方不能同时配置互相冲突的账户 ID。
源码位置:codex-rs/backend-client/src/client.rs :: Client::headers
fn headers(&self) -> HeaderMap {
let mut h = HeaderMap::new();
if let Some(ua) = &self.user_agent {
h.insert(USER_AGENT, ua.clone());
} else {
h.insert(USER_AGENT, HeaderValue::from_static("codex-cli"));
}
self.auth_provider.add_auth_headers(&mut h);
if let Some(acc) = &self.chatgpt_account_id
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = HeaderValue::from_str(acc)
{
h.insert(name, hv);
}
if self.chatgpt_account_is_fedramp
&& let Ok(name) = HeaderName::from_bytes(b"X-OpenAI-Fedramp")
{
h.insert(name, HeaderValue::from_static("true"));
}
h
}6. 账户状态
ConfiguredModelProvider::account_state 是 UI 可见状态,不是认证成功的证明。OpenAI provider 只在 requires_openai_auth 为真时报告账户;API key 映射为 ProviderAccount::ApiKey,ChatGPT 相关变体必须能提供 plan type,否则返回 MissingChatgptAccountDetails。刷新失败时账户会被隐藏,避免把已失效身份显示成有效账户。
Bedrock API key 是故意的边界:普通 OpenAI provider 在 resolve_provider_auth 和 account_state 都拒绝它,并给出“仅 Amazon Bedrock 支持”的错误;只有 Bedrock provider 的专用实现才能消费该变体。
7. 刷新与降级
auth_provider_from_auth_manager 不捕获任意新账户,而是保存启动时的 expected_auth。每次添加 Header 前,它比较 account ID、ChatGPT user ID 和 workspace 标记;身份一致时跟随刷新后的 token,身份变化时返回空 Header。这个不变量阻止“切换账户后沿用旧的 account-scoped client”。
对 ChatGPT session,scoped resolver 还可能先尝试 Agent Identity bootstrap。注册接口不可用时,AgentIdentitySessionFallback 只在当前 session 使用 ChatGPT bearer,并通过原子标志避免后续请求重复 bootstrap;非 bootstrap 错误仍向上传递。
8. 测试与边界
源码测试给出了两组可复核结论。第一组构造 BearerAuthProvider::for_test("access-token", "workspace-123"),断言 Authorization 与 ChatGPT-Account-ID 的精确值,并单独断言 FedRAMP 路由 Header;它证明 Header 拼装,不证明服务端接受凭据。第二组先缓存 test-account,reload 为同账户新 token,再 reload 为 other-account,断言 provider 先跟随刷新、后返回空 Header;它证明身份边界,不证明 OAuth refresh endpoint 本身可用。
第三组让 agent registration 连续返回 503,断言首次解析回退到 bearer、fallback 被 engage,后续解析不再增加注册次数;这证明 session 级降级会在该 session 内持续生效,不代表所有网络错误都可以安全降级。
9. 认证追踪
在源码工作区执行以下只读检查,可以把本文主线重新走一遍:
rg -n "enum CodexAuth|fn auth_mode|fn api_auth_for_scope|fn add_auth_headers" \
codex-rs/login/src/auth/manager.rs \
codex-rs/model-provider/src/{provider.rs,auth.rs,bearer_auth_provider.rs}
cargo test -p codex-model-provider bearer_auth_provider_adds_auth_headers
cargo test -p codex-model-provider auth_manager_provider_follows_refreshes_but_not_account_switches
cargo test -p codex-model-provider chatgpt_bootstrap_unavailable_uses_session_bearer_fallback复述检查:API key 为什么默认进入 OpenAI API URL?为什么 ChatGPT token 可能进入 ChatGPT Codex URL?如果 reload 后账户 ID 改变,哪个对象阻止旧 client 继续发 Header?最后指出模型请求和 Backend Client 各自在哪一行调用 AuthProvider。能回答这些问题,才真正区分了“认证事实”“provider 选路”和“请求消费”三层。
