feat: expose AWS account state from account/read (#19048)

## Why

AWS/Bedrock mode currently reports `account: null` with
`requiresOpenaiAuth: false` from `account/read`. That suppresses the
OpenAI-auth requirement, but it does not let app clients distinguish AWS
auth from any other non-OpenAI custom provider. For the prototype AWS
provider UX, clients need a simple provider-derived signal so they can
suppress ChatGPT/API-key login and token-refresh paths without
hardcoding Bedrock checks.

## What changed

- Adds an `aws` variant to the v2 `Account` protocol union.
- Adds `ProviderAccountKind` to `codex-model-provider` so the runtime
provider owns the app-visible account classification.
- Makes Amazon Bedrock return `ProviderAccountKind::Aws` from the
model-provider layer.
- Updates app-server `account/read` to map `ProviderAccountKind` to the
existing `GetAccountResponse` wire shape.
- Preserves the existing `account: null, requiresOpenaiAuth: false`
behavior for other non-OpenAI providers.
- Regenerates the app-server protocol schema fixtures.
- Adds coverage for provider account classification and for the Amazon
Bedrock `account/read` response.

## Testing

- `cargo test -p codex-model-provider`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-app-server get_account_with_aws_provider`

## Notes

I attempted `just bazel-lock-update` and `just bazel-lock-check`, but
both are blocked in my local environment because `bazel` is not
installed.
This commit is contained in:
Celia Chen
2026-04-24 01:53:13 +00:00
committed by GitHub
parent 72f757d144
commit 432771c5fd
13 changed files with 312 additions and 51 deletions
@@ -9,9 +9,12 @@ use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::account::ProviderAccount;
use codex_protocol::error::Result;
use crate::provider::ModelProvider;
use crate::provider::ProviderAccountResult;
use crate::provider::ProviderAccountState;
use auth::resolve_provider_auth;
use auth::resolve_region;
use mantle::base_url;
@@ -37,6 +40,13 @@ impl ModelProvider for AmazonBedrockModelProvider {
None
}
fn account_state(&self) -> ProviderAccountResult {
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock),
requires_openai_auth: false,
})
}
async fn api_provider(&self) -> Result<Provider> {
let region = resolve_region(&self.aws).await?;
let mut api_provider_info = self.info.clone();
+4
View File
@@ -7,6 +7,10 @@ pub use auth::auth_provider_from_auth;
pub use auth::unauthenticated_auth_provider;
pub use bearer_auth_provider::BearerAuthProvider;
pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider;
pub use codex_protocol::account::ProviderAccount;
pub use provider::ModelProvider;
pub use provider::ProviderAccountError;
pub use provider::ProviderAccountResult;
pub use provider::ProviderAccountState;
pub use provider::SharedModelProvider;
pub use provider::create_model_provider;
+140
View File
@@ -7,11 +7,42 @@ use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::account::ProviderAccount;
use crate::amazon_bedrock::AmazonBedrockModelProvider;
use crate::auth::auth_manager_for_provider;
use crate::auth::resolve_provider_auth;
/// Current app-visible account state for a model provider.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderAccountState {
pub account: Option<ProviderAccount>,
pub requires_openai_auth: bool,
}
/// Error returned when a provider cannot construct its app-visible account state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderAccountError {
MissingChatgptAccountDetails,
}
impl fmt::Display for ProviderAccountError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingChatgptAccountDetails => {
write!(
f,
"email and plan type are required for chatgpt authentication"
)
}
}
}
}
impl std::error::Error for ProviderAccountError {}
pub type ProviderAccountResult = std::result::Result<ProviderAccountState, ProviderAccountError>;
/// Runtime provider abstraction used by model execution.
///
/// Implementations own provider-specific behavior for a model backend. The
@@ -33,6 +64,9 @@ pub trait ModelProvider: fmt::Debug + Send + Sync {
/// Returns the current provider-scoped auth value, if one is configured.
async fn auth(&self) -> Option<CodexAuth>;
/// Returns the current app-visible account state for this provider.
fn account_state(&self) -> ProviderAccountResult;
/// Returns provider configuration adapted for the API client.
async fn api_provider(&self) -> codex_protocol::error::Result<Provider> {
let auth = self.auth().await;
@@ -99,6 +133,38 @@ impl ModelProvider for ConfiguredModelProvider {
None => None,
}
}
fn account_state(&self) -> ProviderAccountResult {
let account = if self.info.requires_openai_auth {
self.auth_manager
.as_ref()
.and_then(|auth_manager| auth_manager.auth_cached())
.map(|auth| match &auth {
CodexAuth::ApiKey(_) => Ok(ProviderAccount::ApiKey),
CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::AgentIdentity(_) => {
let email = auth.get_account_email();
let plan_type = auth.account_plan_type();
match (email, plan_type) {
(Some(email), Some(plan_type)) => {
Ok(ProviderAccount::Chatgpt { email, plan_type })
}
_ => Err(ProviderAccountError::MissingChatgptAccountDetails),
}
}
})
.transpose()?
} else {
None
};
Ok(ProviderAccountState {
account,
requires_openai_auth: self.info.requires_openai_auth,
})
}
}
#[cfg(test)]
@@ -106,7 +172,9 @@ mod tests {
use std::num::NonZeroU64;
use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_model_provider_info::WireApi;
use codex_protocol::config_types::ModelProviderAuthInfo;
use pretty_assertions::assert_eq;
use super::*;
@@ -155,4 +223,76 @@ mod tests {
assert!(provider.auth_manager().is_none());
}
#[test]
fn openai_provider_returns_unauthenticated_openai_account_state() {
let provider = create_model_provider(
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
/*auth_manager*/ None,
);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: None,
requires_openai_auth: true,
})
);
}
#[test]
fn openai_provider_returns_api_key_account_state() {
let provider = create_model_provider(
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key(
"openai-api-key",
))),
);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::ApiKey),
requires_openai_auth: true,
})
);
}
#[test]
fn custom_non_openai_provider_returns_no_account_state() {
let provider = create_model_provider(
ModelProviderInfo {
name: "Custom".to_string(),
base_url: Some("http://localhost:1234/v1".to_string()),
wire_api: WireApi::Responses,
requires_openai_auth: false,
..Default::default()
},
/*auth_manager*/ None,
);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: None,
requires_openai_auth: false,
})
);
}
#[test]
fn amazon_bedrock_provider_returns_bedrock_account_state() {
let provider = create_model_provider(
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
/*auth_manager*/ None,
);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock),
requires_openai_auth: false,
})
);
}
}