模型账户认证共享类型
本文承接MCP与MemoryCitation类型,面向已经理解 Session 和协议事件的读者。这里不把“登录状态”“模型能力”“账户套餐”和“使用量”混成一个对象,而是沿着它们在 Codex 中的实际 owner 阅读:协议 crate 定义可序列化类型,codex-login 持有凭据,Core 把认证和模型信息装配到 turn,App Server 再向客户端投影状态。
这条链尤其容易被误读:AuthMode 不等于 token,PlanType 不等于权限,ModelInfo 不等于当前登录账户,TokenUsageInfo 也不等于 rate limit。它们可以出现在同一批 UI 更新中,但来源和失效时机不同。
1. AuthMode
源码位置:codex-rs/protocol/src/auth.rs :: AuthMode、AuthMode::has_chatgpt_account、AuthMode::uses_codex_backend
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthMode {
ApiKey,
Chatgpt,
#[serde(rename = "chatgptAuthTokens")]
ChatgptAuthTokens,
#[serde(rename = "headers")]
Headers,
#[serde(rename = "agentIdentity")]
AgentIdentity,
#[serde(rename = "personalAccessToken")]
PersonalAccessToken,
#[serde(rename = "bedrockApiKey")]
BedrockApiKey,
#[serde(rename = "bedrockAccessKeys")]
BedrockAccessKeys,
}
impl AuthMode {
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey
| Self::Headers
| Self::AgentIdentity
| Self::BedrockApiKey
| Self::BedrockAccessKeys => false,
}
}
pub fn uses_codex_backend(self) -> bool {
match self {
Self::Chatgpt
| Self::ChatgptAuthTokens
| Self::Headers
| Self::AgentIdentity
| Self::PersonalAccessToken => true,
Self::ApiKey | Self::BedrockApiKey | Self::BedrockAccessKeys => false,
}
}
}0.150.0 的认证模式已经覆盖 API key、托管/外部 ChatGPT token、headers、Agent Identity、personal access token 和 Bedrock。uses_codex_backend 是路由能力判断,不能拿来判断 token 是否存在;has_chatgpt_account 也只是账户语义分类。
2. CodexAuth与凭据
源码位置:codex-rs/login/src/auth/manager.rs :: CodexAuth、CodexAuth::auth_mode、api_auth_mode
#[derive(Debug, Clone)]
pub enum CodexAuth {
ApiKey(ApiKeyAuth),
Chatgpt(ChatgptAuth),
ChatgptAuthTokens(ChatgptAuthTokens),
Headers(AuthHeaders),
AgentIdentity(AgentIdentityAuth),
PersonalAccessToken(PersonalAccessTokenAuth),
BedrockApiKey(BedrockApiKeyAuth),
BedrockAccessKeys(BedrockAccessKeysAuth),
}
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,
Self::BedrockAccessKeys(_) => AuthMode::BedrockAccessKeys,
}
}
pub fn api_auth_mode(&self) -> AuthMode {
match self {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) => AuthMode::Chatgpt,
Self::ChatgptAuthTokens(_) => AuthMode::ChatgptAuthTokens,
Self::Headers(_) => AuthMode::Headers,
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
Self::BedrockAccessKeys(_) => AuthMode::BedrockAccessKeys,
}
}两个 accessor 有意不同:auth_mode() 把外部托管的 ChatGPT token 归一化成 Chatgpt;api_auth_mode() 保留精确凭据来源。App Server 在需要展示具体方式时应使用后者。
源码位置:codex-rs/login/src/auth/manager.rs :: CodexAuth::get_token、get_account_id、account_plan_type
pub fn get_token(&self) -> Result<String, std::io::Error> {
match self {
Self::ApiKey(auth) => Ok(auth.api_key.clone()),
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => {
let access_token = self.get_token_data()?.access_token;
Ok(access_token)
}
Self::AgentIdentity(_) => Err(std::io::Error::other(
"agent identity auth does not expose a bearer token",
)),
Self::Headers(_) => Err(std::io::Error::other(
"header auth does not expose a bearer token",
)),
Self::PersonalAccessToken(auth) => Ok(auth.access_token().to_string()),
Self::BedrockApiKey(_) | Self::BedrockAccessKeys(_) => Err(std::io::Error::other(
"Bedrock API key auth does not expose a Codex bearer token",
)),
}
}
pub fn get_account_id(&self) -> Option<String> {
match self {
Self::Headers(headers) => headers
.headers()
.get("chatgpt-account-id")
.and_then(|value| value.to_str().ok())
.filter(|account_id| !account_id.is_empty() && account_id.trim() == *account_id)
.map(ToOwned::to_owned),
Self::AgentIdentity(auth) => Some(auth.account_id().to_string()),
Self::PersonalAccessToken(auth) => Some(auth.account_id().to_string()),
_ => self.get_current_token_data().and_then(|t| t.account_id),
}
}
pub fn account_plan_type(&self) -> Option<AccountPlanType> {
if matches!(self, Self::Headers(_)) {
return None;
}
if let Self::AgentIdentity(auth) = self {
return Some(auth.plan_type());
}
if let Self::PersonalAccessToken(auth) = self {
return Some(auth.plan_type());
}
self.get_current_token_data().map(|t| {
t.id_token
.chatgpt_plan_type
.map(AccountPlanType::from)
.unwrap_or(AccountPlanType::Unknown)
})
}认证对象可能有 token,也可能只有 headers 或 Agent Identity 元数据。get_token 失败不等于未登录;有些模式故意不暴露 bearer token。账户 ID 和套餐也有各自的来源与缺失规则。
3. 模型与账户
源码位置:codex-rs/protocol/src/openai_models.rs :: ModelInfo
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ModelInfo {
pub slug: String,
pub display_name: String,
pub description: Option<String>,
pub default_reasoning_level: Option<ReasoningEffort>,
pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,
pub shell_type: ConfigShellToolType,
pub visibility: ModelVisibility,
pub supported_in_api: bool,
pub priority: i32,
pub service_tiers: Vec<ModelServiceTier>,
pub context_window: Option<i64>,
pub max_context_window: Option<i64>,
pub input_modalities: Vec<InputModality>,
pub experimental_supported_tools: Vec<String>,
}ModelInfo 描述模型能力和目录属性,包括 reasoning、shell、context window、输入模态和支持的工具。它没有账户 token,也没有当前用户套餐。
源码位置:codex-rs/protocol/src/account.rs :: PlanType、ProviderAccount
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "lowercase")]
pub enum PlanType {
#[default]
Free,
Go,
Plus,
Pro,
ProLite,
Team,
Business,
Enterprise,
Edu,
EduPlus,
EduPro,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderAccount {
ApiKey,
Chatgpt {
email: Option<String>,
plan_type: PlanType,
},
AmazonBedrock {
uses_codex_managed_credentials: bool,
},
}账户层的 PlanType 允许未知值降为 Unknown,用于产品能力和账户展示;它不是 sandbox permission profile,也不是 tool approval。
4. Token与限流状态
源码位置:codex-rs/protocol/src/protocol.rs :: TokenUsageInfo、TokenCountEvent、RateLimitSnapshot
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct TokenUsageInfo {
pub total_token_usage: TokenUsage,
pub last_token_usage: TokenUsage,
#[ts(type = "number | null")]
pub model_context_window: Option<i64>,
}
impl TokenUsageInfo {
pub fn append_last_usage(&mut self, last: &TokenUsage) {
self.total_token_usage.add_assign(last);
self.last_token_usage = last.clone();
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TokenCountEvent {
pub info: Option<TokenUsageInfo>,
pub rate_limits: Option<RateLimitSnapshot>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
pub struct RateLimitSnapshot {
pub limit_id: Option<String>,
pub limit_name: Option<String>,
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub individual_limit: Option<SpendControlLimitSnapshot>,
pub spend_control_reached: Option<bool>,
pub plan_type: Option<crate::account::PlanType>,
pub rate_limit_reached_type: Option<RateLimitReachedType>,
}TokenUsageInfo 同时保存累计值、最近一次值和模型 context window;RateLimitSnapshot 保存窗口、credits、spend-control 和套餐信息。TokenCountEvent 只是把两者一起发布,不代表它们由同一来源计算。
5. App Server状态
源码位置:codex-rs/app-server/src/auth_mode.rs :: auth_mode_to_api
pub(crate) fn auth_mode_to_api(auth_mode: AuthMode) -> ApiAuthMode {
match auth_mode {
AuthMode::ApiKey => ApiAuthMode::ApiKey,
AuthMode::Chatgpt => ApiAuthMode::Chatgpt,
AuthMode::ChatgptAuthTokens => ApiAuthMode::ChatgptAuthTokens,
AuthMode::Headers => ApiAuthMode::Headers,
AuthMode::AgentIdentity => ApiAuthMode::AgentIdentity,
AuthMode::PersonalAccessToken => ApiAuthMode::PersonalAccessToken,
AuthMode::BedrockApiKey => ApiAuthMode::BedrockApiKey,
AuthMode::BedrockAccessKeys => ApiAuthMode::BedrockAccessKeys,
}
}App Server 不直接复用 domain AuthMode,而是显式转换到自己的 wire type。这保持了 crate ownership 边界,也允许 App Server 独立演进协议名称。
源码位置:codex-rs/app-server/src/request_processors/account_processor.rs :: get_auth_status_response
let include_token = params.include_token.unwrap_or(false);
let do_refresh = params.refresh_token.unwrap_or(false);
self.refresh_token_if_requested(do_refresh).await;
let config = self.load_latest_config().await;
let requires_openai_auth = config.model_provider.requires_openai_auth;
let response = if !requires_openai_auth {
GetAuthStatusResponse {
auth_method: None,
auth_token: None,
requires_openai_auth: Some(false),
}
} else {
let auth = if do_refresh {
self.auth_manager.auth_cached()
} else {
self.auth_manager.auth().await
};
match auth {
Some(auth) => {
let permanent_refresh_failure =
self.auth_manager.refresh_failure_for_auth(&auth).is_some();
let auth_mode = auth_mode_to_api(auth.api_auth_mode());
let (reported_auth_method, token_opt) =
if self.auth_manager.is_workload_identity_selected()
|| matches!(
auth,
CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_)
)
|| include_token && permanent_refresh_failure
{
(Some(auth_mode), None)
} else {
match auth.get_token() {
Ok(token) if !token.is_empty() => {
let tok = if include_token { Some(token) } else { None };
(Some(auth_mode), tok)
}
Ok(_) => (None, None),
Err(err) => {
tracing::warn!("failed to get token for auth status: {err}");
(None, None)
}
}
};
GetAuthStatusResponse {
auth_method: reported_auth_method,
auth_token: token_opt,
requires_openai_auth: Some(true),
}
}
None => GetAuthStatusResponse {
auth_method: None,
auth_token: None,
requires_openai_auth: Some(true),
},
}
};auth status 先按 provider 配置判断是否需要 OpenAI auth,再决定是否 refresh、是否读取 cache,以及是否允许把 token 放进 response。headers、Agent Identity、personal access token 等 host-owned/metadata-bearing credentials 不会被导出为 bearer token。
6. TokenCount事件
源码位置:codex-rs/app-server/src/bespoke_event_handling.rs :: TokenCountEvent 处理
let TokenCountEvent { info, rate_limits } = token_count_event;
if let Some(token_usage) = info.map(ThreadTokenUsage::from) {
let notification = ThreadTokenUsageUpdatedNotification {
thread_id: conversation_id.to_string(),
turn_id,
token_usage,
};
outgoing
.send_server_notification(ServerNotification::ThreadTokenUsageUpdated(notification))
.await;
}
if let Some(rate_limits) = rate_limits {
outgoing
.send_server_notification(ServerNotification::AccountRateLimitsUpdated(
AccountRateLimitsUpdatedNotification {
rate_limits: rate_limits.into(),
},
))
.await;
}客户端消费 token usage 和 rate limits 时,应允许任一字段缺失。恢复历史、provider 没有 rate-limit header 或当前 response 没有 usage 时,另一字段仍可能有效。
7. 失败与降级
认证 refresh 可能返回永久失败或暂时 IO 错误;某些 auth mode 没有 bearer token;未知套餐降为 Unknown;模型目录缺字段时 Core 可能使用默认值或 fallback metadata;rate limit 信息缺失时事件中的 rate_limits 为 None。这些降级互不替代:认证失败不是模型不可用的唯一原因,套餐未知也不是未登录。
8. 测试与边界
源码位置:codex-rs/login/src/auth/auth_tests.rs :: loads_api_key_from_auth_json、load_auth_reads_personal_access_token_from_env;codex-rs/protocol/src/account.rs :: business_plan_types_use_expected_wire_names
源码位置:codex-rs/protocol/src/error_tests.rs :: usage_limit_reached_error_formats_rate_limit_reached_types
源码位置:codex-rs/app-server/src/bespoke_event_handling.rs :: test_handle_token_count_event_emits_usage_and_rate_limits
cd codex-rs
cargo test -p codex-login loads_api_key_from_auth_json -- --nocapture --test-threads=1
cargo test -p codex-login load_auth_reads_personal_access_token_from_env -- --nocapture --test-threads=1
cargo test -p codex-protocol business_plan_types_use_expected_wire_names -- --nocapture --test-threads=1
cargo test -p codex-protocol usage_limit_reached_error_formats_rate_limit_reached_types -- --nocapture --test-threads=1
cargo test -p codex-app-server test_handle_token_count_event_emits_usage_and_rate_limits -- --nocapture --test-threads=1这些测试说明认证/套餐转换、限流错误分类和 TokenCount 客户端投影;不能证明真实 OAuth refresh 成功、模型目录永远完整,或客户端计时器与服务端窗口完全同步。
9. 源码定位练习
遇到“已登录但没有 bearer token”,先看 CodexAuth::api_auth_mode 和 get_token 的模式分支;遇到“模型可用但套餐显示未知”,分别检查 ModelInfo 与 account_plan_type,不要把模型目录当作账户服务。
遇到“请求突然受限”,沿 response usage → TokenUsageInfo → RateLimitSnapshot → TokenCountEvent 追踪;遇到“刷新后客户端状态没变”,检查 get_auth_status_response 是否读取 cache、是否 provider 要求 auth,以及 host-owned credentials 是否按设计不导出。
这套共享类型的关键是 owner 分离:登录对象持有凭据,协议对象表达模式和结果,Core 组装当前 turn,App Server 负责公开投影。沿着 owner 阅读,才能判断一次认证、模型或限流问题究竟发生在哪一层。
