mirror of
https://github.com/openai/codex.git
synced 2026-09-03 14:59:03 +00:00
app-server: add managed Bedrock login
This commit is contained in:
@@ -387,6 +387,7 @@ use codex_login::ServerOptions as LoginServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::complete_device_code_login;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::login_with_bedrock_api_key;
|
||||
use codex_login::oauth_client_id;
|
||||
use codex_login::request_device_code;
|
||||
use codex_login::run_login_server;
|
||||
@@ -497,6 +498,7 @@ use codex_app_server_protocol::ServerRequest;
|
||||
|
||||
mod account_processor;
|
||||
mod apps_processor;
|
||||
mod bedrock_auth;
|
||||
mod catalog_processor;
|
||||
mod command_exec_processor;
|
||||
mod config_processor;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::bedrock_auth::set_user_model_provider_to_bedrock;
|
||||
use super::*;
|
||||
use crate::auth_mode::auth_mode_to_api;
|
||||
use crate::external_auth::ExternalAuthBridge;
|
||||
use chrono::DateTime;
|
||||
use codex_model_provider::is_supported_amazon_bedrock_region;
|
||||
|
||||
mod rate_limit_resets;
|
||||
|
||||
@@ -293,8 +295,9 @@ impl AccountRequestProcessor {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::AmazonBedrock { .. } => {
|
||||
return Err(invalid_request("Amazon Bedrock login is not implemented"));
|
||||
LoginAccountParams::AmazonBedrock { api_key, region } => {
|
||||
self.login_amazon_bedrock_v2(request_id, api_key, region)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -359,6 +362,65 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_amazon_bedrock_v2(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
api_key: String,
|
||||
region: String,
|
||||
) {
|
||||
let result = async {
|
||||
if self.auth_manager.is_external_chatgpt_auth_active() {
|
||||
return Err(self.external_auth_active_error());
|
||||
}
|
||||
if matches!(
|
||||
self.config.forced_login_method,
|
||||
Some(ForcedLoginMethod::Chatgpt)
|
||||
) {
|
||||
return Err(invalid_request(
|
||||
"Amazon Bedrock login is disabled. Use ChatGPT login instead.",
|
||||
));
|
||||
}
|
||||
|
||||
let api_key = api_key.trim();
|
||||
if api_key.is_empty() {
|
||||
return Err(invalid_request("Amazon Bedrock API key must not be empty."));
|
||||
}
|
||||
let region = region.trim();
|
||||
if !is_supported_amazon_bedrock_region(region) {
|
||||
return Err(invalid_request(format!(
|
||||
"Amazon Bedrock Mantle does not support region `{region}`"
|
||||
)));
|
||||
}
|
||||
|
||||
{
|
||||
let mut guard = self.active_login.lock().await;
|
||||
if let Some(active) = guard.take() {
|
||||
drop(active);
|
||||
}
|
||||
}
|
||||
|
||||
login_with_bedrock_api_key(
|
||||
&self.config.codex_home,
|
||||
api_key,
|
||||
region,
|
||||
self.config.cli_auth_credentials_store_mode,
|
||||
self.config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.map_err(|err| internal_error(format!("failed to save Amazon Bedrock auth: {err}")))?;
|
||||
set_user_model_provider_to_bedrock(&self.config_manager).await?;
|
||||
self.auth_manager.reload().await;
|
||||
Ok(LoginAccountResponse::AmazonBedrock {})
|
||||
}
|
||||
.await;
|
||||
let logged_in = result.is_ok();
|
||||
self.outgoing.send_result(request_id, result).await;
|
||||
|
||||
if logged_in {
|
||||
self.send_login_success_notifications(/*login_id*/ None)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Build options for a ChatGPT login attempt; performs validation.
|
||||
async fn login_chatgpt_common(
|
||||
&self,
|
||||
|
||||
35
codex-rs/app-server/src/request_processors/bedrock_auth.rs
Normal file
35
codex-rs/app-server/src/request_processors/bedrock_auth.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use super::config_processor::map_error as map_config_error;
|
||||
use crate::config_manager::ConfigManager;
|
||||
use codex_app_server_protocol::ConfigValueWriteParams;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID;
|
||||
|
||||
pub(super) async fn set_user_model_provider_to_bedrock(
|
||||
config_manager: &ConfigManager,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
write_user_model_provider(
|
||||
config_manager,
|
||||
serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
|
||||
/*expected_version*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_user_model_provider(
|
||||
config_manager: &ConfigManager,
|
||||
value: serde_json::Value,
|
||||
expected_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
config_manager
|
||||
.write_value(ConfigValueWriteParams {
|
||||
key_path: "model_provider".to_string(),
|
||||
value,
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
file_path: None,
|
||||
expected_version,
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(map_config_error)
|
||||
}
|
||||
@@ -555,7 +555,7 @@ fn map_network_unix_socket_permission_to_api(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(err: ConfigManagerError) -> JSONRPCErrorError {
|
||||
pub(super) fn map_error(err: ConfigManagerError) -> JSONRPCErrorError {
|
||||
if let Some(code) = err.write_error_code() {
|
||||
return config_write_error(code, err.to_string());
|
||||
}
|
||||
|
||||
@@ -1334,6 +1334,20 @@ impl TestAppServer {
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for managed Amazon Bedrock login.
|
||||
pub async fn send_login_account_amazon_bedrock_request(
|
||||
&mut self,
|
||||
api_key: &str,
|
||||
region: &str,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = serde_json::json!({
|
||||
"type": "amazonBedrock",
|
||||
"apiKey": api_key,
|
||||
"region": region,
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for ChatGPT login.
|
||||
pub async fn send_login_account_chatgpt_request(&mut self) -> anyhow::Result<i64> {
|
||||
let params = serde_json::json!({
|
||||
|
||||
@@ -5,24 +5,30 @@ use app_test_support::to_response;
|
||||
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::ChatGptIdTokenClaims;
|
||||
use app_test_support::DEFAULT_CLIENT_NAME;
|
||||
use app_test_support::encode_id_token;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use app_test_support::write_models_cache;
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::AccountLoginCompletedNotification;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::CancelLoginAccountParams;
|
||||
use codex_app_server_protocol::CancelLoginAccountResponse;
|
||||
use codex_app_server_protocol::CancelLoginAccountStatus;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::GetAccountParams;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
use codex_app_server_protocol::GetAuthStatusParams;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
@@ -33,13 +39,17 @@ use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthDotJson;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_login::auth::BedrockApiKeyAuth;
|
||||
use codex_login::load_auth_dot_json;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::login_with_bedrock_api_key;
|
||||
use codex_protocol::account::AmazonBedrockCredentialSource;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::auth::AuthMode as DomainAuthMode;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -142,6 +152,70 @@ shell_snapshot = false
|
||||
std::fs::write(config_toml, contents)
|
||||
}
|
||||
|
||||
fn read_config_toml(codex_home: &Path) -> Result<toml::Value> {
|
||||
Ok(toml::from_str(&std::fs::read_to_string(
|
||||
codex_home.join("config.toml"),
|
||||
)?)?)
|
||||
}
|
||||
|
||||
fn load_file_auth(codex_home: &Path) -> Result<Option<AuthDotJson>> {
|
||||
Ok(load_auth_dot_json(
|
||||
codex_home,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?)
|
||||
}
|
||||
|
||||
fn aws_managed_bedrock_config() -> CreateConfigTomlParams {
|
||||
CreateConfigTomlParams {
|
||||
model_provider_id: Some("amazon-bedrock".to_string()),
|
||||
extra_provider_config: Some(
|
||||
r#"[model_providers.amazon-bedrock.aws]
|
||||
profile = "codex-bedrock"
|
||||
region = "us-west-2"
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_account(mcp: &mut TestAppServer) -> Result<GetAccountResponse> {
|
||||
let request_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let response = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
to_response(response)
|
||||
}
|
||||
|
||||
async fn assert_account_updated(
|
||||
mcp: &mut TestAppServer,
|
||||
auth_mode: Option<AuthMode>,
|
||||
) -> Result<()> {
|
||||
let notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
let ServerNotification::AccountUpdated(payload) = notification.try_into()? else {
|
||||
bail!("unexpected notification")
|
||||
};
|
||||
assert_eq!(
|
||||
payload,
|
||||
AccountUpdatedNotification {
|
||||
auth_mode,
|
||||
plan_type: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mock_device_code_usercode(server: &MockServer, interval_seconds: u64) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/accounts/deviceauth/usercode"))
|
||||
@@ -1011,6 +1085,368 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test-key",
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let mut expected_config = read_config_toml(codex_home.path())?;
|
||||
expected_config
|
||||
.as_table_mut()
|
||||
.expect("config should be a table")
|
||||
.insert(
|
||||
"model_provider".to_string(),
|
||||
toml::Value::String("amazon-bedrock".to_string()),
|
||||
);
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request(" managed-bedrock-api-key ", " us-west-2 ")
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(response)?,
|
||||
LoginAccountResponse::AmazonBedrock {}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
load_file_auth(codex_home.path())?,
|
||||
Some(AuthDotJson {
|
||||
auth_mode: Some(DomainAuthMode::BedrockApiKey),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: Some(BedrockApiKeyAuth {
|
||||
api_key: "managed-bedrock-api-key".to_string(),
|
||||
region: "us-west-2".to_string(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert_eq!(read_config_toml(codex_home.path())?, expected_config);
|
||||
|
||||
let notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/login/completed"),
|
||||
)
|
||||
.await??;
|
||||
let ServerNotification::AccountLoginCompleted(payload) = notification.try_into()? else {
|
||||
bail!("unexpected notification")
|
||||
};
|
||||
assert_eq!(
|
||||
payload,
|
||||
AccountLoginCompletedNotification {
|
||||
login_id: None,
|
||||
success: true,
|
||||
error: None,
|
||||
}
|
||||
);
|
||||
assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_bedrock_login_requires_experimental_api() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let initialized = mcp
|
||||
.initialize_with_capabilities(
|
||||
ClientInfo {
|
||||
name: DEFAULT_CLIENT_NAME.to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(matches!(initialized, JSONRPCMessage::Response(_)));
|
||||
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"account/login/start.amazonBedrock requires experimentalApi capability"
|
||||
);
|
||||
assert_eq!(load_file_auth(codex_home.path())?, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_managed_bedrock_updates_active_bedrock_account() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), aws_managed_bedrock_config())?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(response)?,
|
||||
LoginAccountResponse::AmazonBedrock {}
|
||||
);
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/login/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?;
|
||||
assert_eq!(
|
||||
read_account(&mut mcp).await?,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::AmazonBedrock {
|
||||
credential_source: AmazonBedrockCredentialSource::CodexManaged,
|
||||
}),
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
|
||||
assert!(codex_home.path().join("auth.json").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_account_amazon_bedrock_rejects_invalid_credentials_without_changes() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let expected_config = read_config_toml(codex_home.path())?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request(" ", "us-west-2")
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"Amazon Bedrock API key must not be empty."
|
||||
);
|
||||
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-1")
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"Amazon Bedrock Mantle does not support region `us-west-1`"
|
||||
);
|
||||
assert_eq!(load_file_auth(codex_home.path())?, None);
|
||||
assert_eq!(read_config_toml(codex_home.path())?, expected_config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_account_amazon_bedrock_persists_under_provider_override() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_args(
|
||||
codex_home.path(),
|
||||
&["-c", "model_provider=\"mock_provider\""],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let mut expected_config = read_config_toml(codex_home.path())?;
|
||||
expected_config
|
||||
.as_table_mut()
|
||||
.expect("config should be a table")
|
||||
.insert(
|
||||
"model_provider".to_string(),
|
||||
toml::Value::String("amazon-bedrock".to_string()),
|
||||
);
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(response)?,
|
||||
LoginAccountResponse::AmazonBedrock {}
|
||||
);
|
||||
assert_eq!(
|
||||
load_file_auth(codex_home.path())?,
|
||||
Some(AuthDotJson {
|
||||
auth_mode: Some(DomainAuthMode::BedrockApiKey),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: Some(BedrockApiKeyAuth {
|
||||
api_key: "managed-bedrock-api-key".to_string(),
|
||||
region: "us-west-2".to_string(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert_eq!(read_config_toml(codex_home.path())?, expected_config);
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/login/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?;
|
||||
assert_eq!(
|
||||
read_account(&mut mcp).await?,
|
||||
GetAccountResponse {
|
||||
account: None,
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_account_amazon_bedrock_rejected_when_forced_chatgpt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
forced_method: Some("chatgpt".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"Amazon Bedrock login is disabled. Use ChatGPT login instead."
|
||||
);
|
||||
assert_eq!(load_file_auth(codex_home.path())?, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_account_amazon_bedrock_allowed_when_forced_api() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
forced_method: Some("api".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
|
||||
.await?;
|
||||
let response = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(response)?,
|
||||
LoginAccountResponse::AmazonBedrock {}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_account_amazon_bedrock_rejected_with_external_chatgpt_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
let access_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("embedded@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let set_id = mcp
|
||||
.send_chatgpt_auth_tokens_login_request(
|
||||
access_token,
|
||||
WORKSPACE_ID_EMBEDDED.to_string(),
|
||||
Some("pro".to_string()),
|
||||
)
|
||||
.await?;
|
||||
let set_response = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(set_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(set_response)?,
|
||||
LoginAccountResponse::ChatgptAuthTokens {}
|
||||
);
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"External auth is active. Use account/login/start (chatgptAuthTokens) to update it or account/logout to clear it."
|
||||
);
|
||||
assert_eq!(load_file_auth(codex_home.path())?, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -39,8 +39,13 @@ pub(super) fn region_from_config(aws: &ModelProviderAwsAuthInfo) -> Option<Strin
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
/// Returns whether Amazon Bedrock Mantle is available in `region`.
|
||||
pub fn is_supported_amazon_bedrock_region(region: &str) -> bool {
|
||||
BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion)
|
||||
}
|
||||
|
||||
pub(super) fn base_url(region: &str) -> Result<String> {
|
||||
if BEDROCK_MANTLE_SUPPORTED_REGIONS.contains(®ion) {
|
||||
if is_supported_amazon_bedrock_region(region) {
|
||||
Ok(format!("https://bedrock-mantle.{region}.api.aws/openai/v1"))
|
||||
} else {
|
||||
Err(CodexErr::Fatal(format!(
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::provider::ProviderCapabilities;
|
||||
use auth::resolve_provider_auth;
|
||||
pub(crate) use catalog::static_model_catalog;
|
||||
use catalog::with_default_only_service_tier;
|
||||
pub use mantle::is_supported_amazon_bedrock_region;
|
||||
use mantle::runtime_base_url;
|
||||
|
||||
/// Runtime provider for Amazon Bedrock's OpenAI-compatible Mantle endpoint.
|
||||
|
||||
@@ -4,6 +4,7 @@ mod bearer_auth_provider;
|
||||
mod models_endpoint;
|
||||
mod provider;
|
||||
|
||||
pub use amazon_bedrock::is_supported_amazon_bedrock_region;
|
||||
pub use auth::AgentIdentitySessionFallback;
|
||||
pub use auth::ProviderAuthScope;
|
||||
pub use auth::ResolvedProviderAuth;
|
||||
@@ -12,6 +13,7 @@ pub use auth::auth_provider_from_auth_manager;
|
||||
pub use auth::unauthenticated_auth_provider;
|
||||
pub use bearer_auth_provider::BearerAuthProvider;
|
||||
pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider;
|
||||
pub use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
|
||||
pub use codex_model_provider_info::CHATGPT_CODEX_BASE_URL;
|
||||
pub use codex_protocol::account::ProviderAccount;
|
||||
pub use provider::ModelProvider;
|
||||
|
||||
Reference in New Issue
Block a user