mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
[Codex][Codex CLI] Park trimmed auth observability slice
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -179,6 +179,7 @@ use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::Cursor as RolloutCursor;
|
||||
use codex_core::ModelProviderInfo;
|
||||
use codex_core::NewThread;
|
||||
use codex_core::RolloutRecorder;
|
||||
use codex_core::SessionMeta;
|
||||
@@ -241,7 +242,6 @@ use codex_login::ServerOptions as LoginServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::run_login_server;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -317,9 +317,6 @@ use crate::thread_state::ThreadStateManager;
|
||||
|
||||
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_LIST_MAX_LIMIT: usize = 100;
|
||||
const APP_SERVER_AUTH_STATUS_EVENT: &str = "codex.app_server_auth_status";
|
||||
const APP_SERVER_AUTH_CACHE_EVENT: &str = "codex.app_server_auth_cache";
|
||||
const APP_SERVER_AUTH_NOTIFICATION_EVENT: &str = "codex.app_server_auth_notification";
|
||||
|
||||
struct ThreadListFilters {
|
||||
model_providers: Option<Vec<String>>,
|
||||
@@ -416,174 +413,29 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AppServerAuthStatusObservation {
|
||||
provider_id: String,
|
||||
fn app_server_feedback_auth_state(
|
||||
requires_openai_auth: bool,
|
||||
auth_method: String,
|
||||
auth_token_present: bool,
|
||||
include_token_requested: bool,
|
||||
refresh_token_requested: bool,
|
||||
decision_state: String,
|
||||
}
|
||||
auth: Option<&CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> &'static str {
|
||||
let has_required_auth = if requires_openai_auth {
|
||||
auth.is_some()
|
||||
|| provider.api_key().ok().flatten().is_some()
|
||||
|| provider
|
||||
.experimental_bearer_token
|
||||
.as_ref()
|
||||
.is_some_and(|token| !token.trim().is_empty())
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AppServerAuthCacheObservation {
|
||||
action: String,
|
||||
reason: String,
|
||||
auth_method: String,
|
||||
token_present: bool,
|
||||
account_id_present: bool,
|
||||
plan_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AppServerAuthNotificationObservation {
|
||||
method: String,
|
||||
followup_auth_check_started: bool,
|
||||
followup_auth_check_completed: bool,
|
||||
followup_requires_openai_auth: bool,
|
||||
followup_auth_method: String,
|
||||
followup_auth_token_present: bool,
|
||||
}
|
||||
|
||||
fn normalize_app_server_auth_method(auth_method: Option<AuthMode>) -> String {
|
||||
match auth_method {
|
||||
Some(AuthMode::ApiKey) => "api_key".to_string(),
|
||||
Some(AuthMode::Chatgpt) => "chatgpt".to_string(),
|
||||
Some(AuthMode::ChatgptAuthTokens) => "chatgpt_auth_tokens".to_string(),
|
||||
None => "none".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_core_auth_method(auth: Option<&CodexAuth>) -> String {
|
||||
match auth.map(CodexAuth::api_auth_mode) {
|
||||
Some(AuthMode::ApiKey) => "api_key".to_string(),
|
||||
Some(AuthMode::Chatgpt) => "chatgpt".to_string(),
|
||||
Some(AuthMode::ChatgptAuthTokens) => "chatgpt_auth_tokens".to_string(),
|
||||
None => "none".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_plan_type(auth: Option<&CodexAuth>) -> String {
|
||||
match auth.and_then(CodexAuth::account_plan_type) {
|
||||
Some(AccountPlanType::Free) => "free".to_string(),
|
||||
Some(AccountPlanType::Go) => "go".to_string(),
|
||||
Some(AccountPlanType::Plus) => "plus".to_string(),
|
||||
Some(AccountPlanType::Pro) => "pro".to_string(),
|
||||
Some(AccountPlanType::Team) => "team".to_string(),
|
||||
Some(AccountPlanType::Business) => "business".to_string(),
|
||||
Some(AccountPlanType::Enterprise) => "enterprise".to_string(),
|
||||
Some(AccountPlanType::Edu) => "edu".to_string(),
|
||||
Some(AccountPlanType::Unknown) => "unknown".to_string(),
|
||||
None => "absent".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_token_present(auth: Option<&CodexAuth>) -> bool {
|
||||
auth.and_then(|value| value.get_token().ok())
|
||||
.is_some_and(|token| !token.is_empty())
|
||||
}
|
||||
|
||||
fn build_auth_status_observation(
|
||||
provider_id: &str,
|
||||
response: &GetAuthStatusResponse,
|
||||
include_token_requested: bool,
|
||||
refresh_token_requested: bool,
|
||||
) -> AppServerAuthStatusObservation {
|
||||
let requires_openai_auth = response.requires_openai_auth.unwrap_or(true);
|
||||
let auth_token_present = response.auth_method.is_some()
|
||||
|| response
|
||||
.auth_token
|
||||
.as_ref()
|
||||
.is_some_and(|token| !token.is_empty());
|
||||
let decision_state = if !requires_openai_auth || auth_token_present {
|
||||
if has_required_auth {
|
||||
"connected"
|
||||
} else {
|
||||
"unauthed"
|
||||
};
|
||||
|
||||
AppServerAuthStatusObservation {
|
||||
provider_id: provider_id.to_string(),
|
||||
requires_openai_auth,
|
||||
auth_method: normalize_app_server_auth_method(response.auth_method),
|
||||
auth_token_present,
|
||||
include_token_requested,
|
||||
refresh_token_requested,
|
||||
decision_state: decision_state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_auth_cache_observation(
|
||||
action: &str,
|
||||
reason: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> AppServerAuthCacheObservation {
|
||||
AppServerAuthCacheObservation {
|
||||
action: action.to_string(),
|
||||
reason: reason.to_string(),
|
||||
auth_method: normalize_core_auth_method(auth),
|
||||
token_present: auth_token_present(auth),
|
||||
account_id_present: auth.and_then(CodexAuth::get_account_id).is_some(),
|
||||
plan_type: normalize_plan_type(auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_auth_notification_observation(
|
||||
method: &str,
|
||||
requires_openai_auth: bool,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> AppServerAuthNotificationObservation {
|
||||
AppServerAuthNotificationObservation {
|
||||
method: method.to_string(),
|
||||
followup_auth_check_started: true,
|
||||
followup_auth_check_completed: true,
|
||||
followup_requires_openai_auth: requires_openai_auth,
|
||||
followup_auth_method: normalize_core_auth_method(auth),
|
||||
followup_auth_token_present: auth_token_present(auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_auth_status_observation(observation: &AppServerAuthStatusObservation) {
|
||||
tracing::info!(
|
||||
event.name = APP_SERVER_AUTH_STATUS_EVENT,
|
||||
provider_id = %observation.provider_id,
|
||||
requires_openai_auth = observation.requires_openai_auth,
|
||||
auth_method = %observation.auth_method,
|
||||
auth_token_present = observation.auth_token_present,
|
||||
include_token_requested = observation.include_token_requested,
|
||||
refresh_token_requested = observation.refresh_token_requested,
|
||||
decision_state = %observation.decision_state,
|
||||
"codex app-server auth status resolved",
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_auth_cache_observation(observation: &AppServerAuthCacheObservation) {
|
||||
tracing::info!(
|
||||
event.name = APP_SERVER_AUTH_CACHE_EVENT,
|
||||
action = %observation.action,
|
||||
reason = %observation.reason,
|
||||
auth_method = %observation.auth_method,
|
||||
token_present = observation.token_present,
|
||||
account_id_present = observation.account_id_present,
|
||||
plan_type = %observation.plan_type,
|
||||
"codex app-server auth cache changed",
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_auth_notification_observation(observation: &AppServerAuthNotificationObservation) {
|
||||
tracing::info!(
|
||||
event.name = APP_SERVER_AUTH_NOTIFICATION_EVENT,
|
||||
method = %observation.method,
|
||||
followup_auth_check_started = observation.followup_auth_check_started,
|
||||
followup_auth_check_completed = observation.followup_auth_check_completed,
|
||||
followup_requires_openai_auth = observation.followup_requires_openai_auth,
|
||||
followup_auth_method = %observation.followup_auth_method,
|
||||
followup_auth_token_present = observation.followup_auth_token_present,
|
||||
"codex app-server auth notification follow-up resolved",
|
||||
);
|
||||
}
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(crate) fn clear_plugin_related_caches(&self) {
|
||||
self.thread_manager.plugins_manager().clear_cache();
|
||||
@@ -1151,7 +1003,6 @@ impl CodexMessageProcessor {
|
||||
) {
|
||||
match self.login_api_key_common(¶ms).await {
|
||||
Ok(()) => {
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
let response = codex_app_server_protocol::LoginAccountResponse::ApiKey {};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
|
||||
@@ -1165,27 +1016,12 @@ impl CodexMessageProcessor {
|
||||
payload_login_completed,
|
||||
))
|
||||
.await;
|
||||
emit_auth_cache_observation(&build_auth_cache_observation(
|
||||
"set",
|
||||
"login_completed",
|
||||
auth.as_ref(),
|
||||
));
|
||||
emit_auth_notification_observation(&build_auth_notification_observation(
|
||||
"account/login/completed",
|
||||
self.config.model_provider.requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
));
|
||||
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
emit_auth_notification_observation(&build_auth_notification_observation(
|
||||
"account/updated",
|
||||
self.config.model_provider.requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -1249,7 +1085,6 @@ impl CodexMessageProcessor {
|
||||
let chatgpt_base_url = self.config.chatgpt_base_url.clone();
|
||||
let codex_home = self.config.codex_home.clone();
|
||||
let cli_overrides = self.cli_overrides.clone();
|
||||
let requires_openai_auth = self.config.model_provider.requires_openai_auth;
|
||||
let auth_url = server.auth_url.clone();
|
||||
tokio::spawn(async move {
|
||||
let (success, error_msg) = match tokio::time::timeout(
|
||||
@@ -1293,18 +1128,6 @@ impl CodexMessageProcessor {
|
||||
|
||||
// Notify clients with the actual current auth mode.
|
||||
let auth = auth_manager.auth_cached();
|
||||
emit_auth_cache_observation(&build_auth_cache_observation(
|
||||
"set",
|
||||
"login_completed",
|
||||
auth.as_ref(),
|
||||
));
|
||||
emit_auth_notification_observation(
|
||||
&build_auth_notification_observation(
|
||||
"account/login/completed",
|
||||
requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
),
|
||||
);
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
@@ -1314,13 +1137,6 @@ impl CodexMessageProcessor {
|
||||
payload_v2,
|
||||
))
|
||||
.await;
|
||||
emit_auth_notification_observation(
|
||||
&build_auth_notification_observation(
|
||||
"account/updated",
|
||||
requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Clear the active login if it matches this attempt. It may have been replaced or cancelled.
|
||||
@@ -1450,7 +1266,6 @@ impl CodexMessageProcessor {
|
||||
return;
|
||||
}
|
||||
self.auth_manager.reload();
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
replace_cloud_requirements_loader(
|
||||
self.cloud_requirements.as_ref(),
|
||||
self.auth_manager.clone(),
|
||||
@@ -1477,27 +1292,12 @@ impl CodexMessageProcessor {
|
||||
payload_login_completed,
|
||||
))
|
||||
.await;
|
||||
emit_auth_cache_observation(&build_auth_cache_observation(
|
||||
"set",
|
||||
"login_completed",
|
||||
auth.as_ref(),
|
||||
));
|
||||
emit_auth_notification_observation(&build_auth_notification_observation(
|
||||
"account/login/completed",
|
||||
self.config.model_provider.requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
));
|
||||
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
emit_auth_notification_observation(&build_auth_notification_observation(
|
||||
"account/updated",
|
||||
self.config.model_provider.requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
));
|
||||
}
|
||||
|
||||
async fn logout_common(&mut self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
|
||||
@@ -1539,11 +1339,6 @@ impl CodexMessageProcessor {
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(payload_v2))
|
||||
.await;
|
||||
emit_auth_cache_observation(&build_auth_cache_observation(
|
||||
"clear",
|
||||
"logout",
|
||||
self.auth_manager.auth_cached().as_ref(),
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -1559,11 +1354,6 @@ impl CodexMessageProcessor {
|
||||
if let Err(err) = self.auth_manager.refresh_token().await {
|
||||
tracing::warn!("failed to refresh token while getting account: {err}");
|
||||
}
|
||||
emit_auth_cache_observation(&build_auth_cache_observation(
|
||||
"refresh",
|
||||
"refresh_requested",
|
||||
self.auth_manager.auth_cached().as_ref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1613,12 +1403,6 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
emit_auth_status_observation(&build_auth_status_observation(
|
||||
self.config.model_provider_id.as_str(),
|
||||
&response,
|
||||
include_token,
|
||||
do_refresh,
|
||||
));
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
|
||||
@@ -7139,6 +6923,18 @@ impl CodexMessageProcessor {
|
||||
{
|
||||
tracing::info!(target: "feedback_tags", chatgpt_user_id);
|
||||
}
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
let requires_openai_auth = self.config.model_provider.requires_openai_auth;
|
||||
tracing::info!(
|
||||
target: "feedback_tags",
|
||||
app_server_auth_state =
|
||||
app_server_feedback_auth_state(
|
||||
requires_openai_auth,
|
||||
auth.as_ref(),
|
||||
&self.config.model_provider,
|
||||
),
|
||||
app_server_requires_openai_auth = requires_openai_auth,
|
||||
);
|
||||
let snapshot = self.feedback.snapshot(conversation_id);
|
||||
let thread_id = snapshot.thread_id.clone();
|
||||
let sqlite_feedback_logs = if include_logs {
|
||||
@@ -8376,7 +8172,6 @@ mod tests {
|
||||
use crate::outgoing_message::OutgoingEnvelope;
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
use anyhow::Result;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -8768,87 +8563,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_auth_status_observation_marks_unauthed_when_auth_is_required_but_missing() {
|
||||
let observation = build_auth_status_observation(
|
||||
"openai",
|
||||
&GetAuthStatusResponse {
|
||||
auth_method: None,
|
||||
auth_token: None,
|
||||
requires_openai_auth: Some(true),
|
||||
},
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
observation,
|
||||
AppServerAuthStatusObservation {
|
||||
provider_id: "openai".to_string(),
|
||||
requires_openai_auth: true,
|
||||
auth_method: "none".to_string(),
|
||||
auth_token_present: false,
|
||||
include_token_requested: false,
|
||||
refresh_token_requested: false,
|
||||
decision_state: "unauthed".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_auth_status_observation_marks_connected_when_openai_auth_is_not_required() {
|
||||
let observation = build_auth_status_observation(
|
||||
"mock_provider",
|
||||
&GetAuthStatusResponse {
|
||||
auth_method: None,
|
||||
auth_token: None,
|
||||
requires_openai_auth: Some(false),
|
||||
},
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
assert_eq!(observation.decision_state, "connected");
|
||||
assert_eq!(observation.auth_method, "none");
|
||||
assert_eq!(observation.requires_openai_auth, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_auth_cache_observation_normalizes_present_chatgpt_auth() {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let observation = build_auth_cache_observation("refresh", "refresh_requested", Some(&auth));
|
||||
|
||||
assert_eq!(
|
||||
observation,
|
||||
AppServerAuthCacheObservation {
|
||||
action: "refresh".to_string(),
|
||||
reason: "refresh_requested".to_string(),
|
||||
auth_method: "chatgpt".to_string(),
|
||||
token_present: true,
|
||||
account_id_present: true,
|
||||
plan_type: "unknown".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_auth_notification_observation_uses_current_auth_snapshot() {
|
||||
let auth = CodexAuth::from_api_key("sk-test-key");
|
||||
let observation = build_auth_notification_observation("account/updated", true, Some(&auth));
|
||||
|
||||
assert_eq!(
|
||||
observation,
|
||||
AppServerAuthNotificationObservation {
|
||||
method: "account/updated".to_string(),
|
||||
followup_auth_check_started: true,
|
||||
followup_auth_check_completed: true,
|
||||
followup_requires_openai_auth: true,
|
||||
followup_auth_method: "api_key".to_string(),
|
||||
followup_auth_token_present: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_thread_state_clears_listener_and_active_turn_history() -> Result<()> {
|
||||
let manager = ThreadStateManager::new();
|
||||
|
||||
@@ -1244,10 +1244,6 @@ impl AuthManager {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn codex_api_key_env_enabled(&self) -> bool {
|
||||
self.enable_codex_api_key_env
|
||||
}
|
||||
|
||||
pub fn is_external_auth_active(&self) -> bool {
|
||||
self.auth_cached()
|
||||
.as_ref()
|
||||
|
||||
@@ -26,12 +26,6 @@ impl AuthEnvTelemetry {
|
||||
refresh_token_url_override_present: self.refresh_token_url_override_present,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_api_key_override(&self) -> bool {
|
||||
self.openai_api_key_env_present
|
||||
|| (self.codex_api_key_env_enabled && self.codex_api_key_env_present)
|
||||
|| self.provider_env_key_present == Some(true)
|
||||
}
|
||||
}
|
||||
pub(crate) fn collect_auth_env_telemetry(
|
||||
provider: &ModelProviderInfo,
|
||||
|
||||
+53
-131
@@ -95,7 +95,6 @@ use crate::client_common::ResponseEvent;
|
||||
use crate::client_common::ResponseStream;
|
||||
use crate::config::Config;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::residency_header_telemetry_for_provider_headers;
|
||||
use crate::endpoint_config_telemetry::EndpointConfigTelemetry;
|
||||
use crate::endpoint_config_telemetry::EndpointConfigTelemetrySource;
|
||||
use crate::error::CodexErr;
|
||||
@@ -121,6 +120,10 @@ const RESPONSES_WEBSOCKETS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=20
|
||||
const RESPONSES_ENDPOINT: &str = "/responses";
|
||||
const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact";
|
||||
const MEMORIES_SUMMARIZE_ENDPOINT: &str = "/memories/trace_summarize";
|
||||
#[cfg(test)]
|
||||
pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration =
|
||||
Duration::from_millis(crate::model_provider_info::DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS);
|
||||
|
||||
pub fn ws_version_from_features(config: &Config) -> bool {
|
||||
config
|
||||
.features
|
||||
@@ -166,18 +169,11 @@ struct CurrentClientSetup {
|
||||
#[derive(Clone)]
|
||||
struct RequestRouteTelemetry {
|
||||
endpoint: &'static str,
|
||||
residency_header_attached: bool,
|
||||
residency_header_value: Option<String>,
|
||||
}
|
||||
|
||||
impl RequestRouteTelemetry {
|
||||
fn for_endpoint(endpoint: &'static str, provider_headers: &ApiHeaderMap) -> Self {
|
||||
let residency = residency_header_telemetry_for_provider_headers(provider_headers);
|
||||
Self {
|
||||
endpoint,
|
||||
residency_header_attached: residency.attached,
|
||||
residency_header_value: residency.value,
|
||||
}
|
||||
fn for_endpoint(endpoint: &'static str, _provider_headers: &ApiHeaderMap) -> Self {
|
||||
Self { endpoint }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,35 +291,6 @@ impl ModelClient {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_with_provider_id(
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
conversation_id: ThreadId,
|
||||
provider_id: &str,
|
||||
provider: ModelProviderInfo,
|
||||
session_source: SessionSource,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
responses_websockets_enabled_by_feature: bool,
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
beta_features_header: Option<String>,
|
||||
) -> Self {
|
||||
let endpoint_telemetry_source =
|
||||
EndpointConfigTelemetrySource::for_provider(provider_id, &provider);
|
||||
Self::new_with_endpoint_telemetry_source(
|
||||
auth_manager,
|
||||
conversation_id,
|
||||
provider,
|
||||
endpoint_telemetry_source,
|
||||
session_source,
|
||||
model_verbosity,
|
||||
responses_websockets_enabled_by_feature,
|
||||
enable_request_compression,
|
||||
include_timing_metrics,
|
||||
beta_features_header,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new_with_endpoint_telemetry_source(
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
@@ -646,15 +613,22 @@ impl ModelClient {
|
||||
provider_header_names.map(str::to_owned),
|
||||
request_route_telemetry.clone(),
|
||||
);
|
||||
let websocket_connect_timeout = self.state.provider.websocket_connect_timeout();
|
||||
let start = Instant::now();
|
||||
let result = ApiWebSocketResponsesClient::new(api_provider, api_auth)
|
||||
.connect(
|
||||
let result = match tokio::time::timeout(
|
||||
websocket_connect_timeout,
|
||||
ApiWebSocketResponsesClient::new(api_provider, api_auth).connect(
|
||||
headers,
|
||||
crate::default_client::default_headers(),
|
||||
turn_state,
|
||||
Some(websocket_telemetry),
|
||||
)
|
||||
.await;
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(ApiError::Transport(TransportError::Timeout)),
|
||||
};
|
||||
let error_message = result.as_ref().err().map(telemetry_api_error_message);
|
||||
let response_debug = result
|
||||
.as_ref()
|
||||
@@ -662,7 +636,7 @@ impl ModelClient {
|
||||
.map(extract_response_debug_context_from_api_error)
|
||||
.unwrap_or_default();
|
||||
let status = result.as_ref().err().and_then(api_error_http_status);
|
||||
session_telemetry.record_websocket_connect_with_endpoint_details(
|
||||
session_telemetry.record_websocket_connect(
|
||||
start.elapsed(),
|
||||
status,
|
||||
error_message.as_deref(),
|
||||
@@ -672,8 +646,6 @@ impl ModelClient {
|
||||
auth_context.recovery_mode,
|
||||
auth_context.recovery_phase,
|
||||
request_route_telemetry.endpoint,
|
||||
request_route_telemetry.residency_header_attached,
|
||||
request_route_telemetry.residency_header_value.as_deref(),
|
||||
provider_header_names,
|
||||
endpoint_telemetry.base_url_origin,
|
||||
endpoint_telemetry.host_class,
|
||||
@@ -723,20 +695,19 @@ impl ModelClient {
|
||||
auth_recovery_mode: auth_context.recovery_mode,
|
||||
auth_recovery_phase: auth_context.recovery_phase,
|
||||
auth_connection_reused: Some(false),
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names,
|
||||
base_url_origin: endpoint_telemetry.base_url_origin,
|
||||
host_class: endpoint_telemetry.host_class,
|
||||
base_url_source: endpoint_telemetry.base_url_source,
|
||||
base_url_is_default: endpoint_telemetry.base_url_is_default,
|
||||
residency_header_attached: Some(request_route_telemetry.residency_header_attached),
|
||||
residency_header_value: request_route_telemetry.residency_header_value.as_deref(),
|
||||
auth_request_id: response_debug.request_id.as_deref(),
|
||||
auth_cf_ray: response_debug.cf_ray.as_deref(),
|
||||
auth_error: response_debug.auth_error.as_deref(),
|
||||
auth_error_code: response_debug.auth_error_code.as_deref(),
|
||||
error_body_class: response_debug.error_body_class,
|
||||
safe_error_message: response_debug.safe_error_message,
|
||||
geo_denial_detected: Some(response_debug.geo_denial_detected),
|
||||
auth_recovery_followup_success: auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(result.is_ok()),
|
||||
@@ -745,23 +716,6 @@ impl ModelClient {
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
});
|
||||
if status == Some(StatusCode::UNAUTHORIZED.as_u16()) && response_debug.geo_denial_detected {
|
||||
session_telemetry.record_geo_denial(
|
||||
request_route_telemetry.endpoint,
|
||||
auth_context.auth_header_attached,
|
||||
auth_context.auth_header_name,
|
||||
request_route_telemetry.residency_header_attached,
|
||||
request_route_telemetry.residency_header_value.as_deref(),
|
||||
provider_header_names,
|
||||
status,
|
||||
response_debug.request_id.as_deref(),
|
||||
response_debug.cf_ray.as_deref(),
|
||||
response_debug.auth_error.as_deref(),
|
||||
response_debug.auth_error_code.as_deref(),
|
||||
response_debug.error_body_class.unwrap_or_default(),
|
||||
response_debug.safe_error_message,
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1825,40 +1779,35 @@ impl RequestTelemetry for ApiTelemetry {
|
||||
let debug = error
|
||||
.map(extract_response_debug_context)
|
||||
.unwrap_or_default();
|
||||
self.session_telemetry
|
||||
.record_api_request_with_endpoint_details(
|
||||
attempt,
|
||||
status,
|
||||
error_message.as_deref(),
|
||||
duration,
|
||||
self.auth_context.auth_header_attached,
|
||||
self.auth_context.auth_header_name,
|
||||
self.auth_context.retry_after_unauthorized,
|
||||
self.auth_context.recovery_mode,
|
||||
self.auth_context.recovery_phase,
|
||||
self.request_route_telemetry.endpoint,
|
||||
self.request_route_telemetry.residency_header_attached,
|
||||
self.request_route_telemetry
|
||||
.residency_header_value
|
||||
.as_deref(),
|
||||
self.provider_header_names.as_deref(),
|
||||
self.endpoint_telemetry.base_url_origin,
|
||||
self.endpoint_telemetry.host_class,
|
||||
self.endpoint_telemetry.base_url_source,
|
||||
self.endpoint_telemetry.base_url_is_default,
|
||||
self.auth_env_telemetry.openai_api_key_env_present,
|
||||
self.auth_env_telemetry.codex_api_key_env_present,
|
||||
self.auth_env_telemetry.codex_api_key_env_enabled,
|
||||
self.auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
self.auth_env_telemetry.provider_env_key_present,
|
||||
self.auth_env_telemetry.refresh_token_url_override_present,
|
||||
debug.request_id.as_deref(),
|
||||
debug.cf_ray.as_deref(),
|
||||
debug.auth_error.as_deref(),
|
||||
debug.auth_error_code.as_deref(),
|
||||
debug.error_body_class,
|
||||
debug.safe_error_message,
|
||||
);
|
||||
self.session_telemetry.record_api_request(
|
||||
attempt,
|
||||
status,
|
||||
error_message.as_deref(),
|
||||
duration,
|
||||
self.auth_context.auth_header_attached,
|
||||
self.auth_context.auth_header_name,
|
||||
self.auth_context.retry_after_unauthorized,
|
||||
self.auth_context.recovery_mode,
|
||||
self.auth_context.recovery_phase,
|
||||
self.request_route_telemetry.endpoint,
|
||||
self.provider_header_names.as_deref(),
|
||||
self.endpoint_telemetry.base_url_origin,
|
||||
self.endpoint_telemetry.host_class,
|
||||
self.endpoint_telemetry.base_url_source,
|
||||
self.endpoint_telemetry.base_url_is_default,
|
||||
self.auth_env_telemetry.openai_api_key_env_present,
|
||||
self.auth_env_telemetry.codex_api_key_env_present,
|
||||
self.auth_env_telemetry.codex_api_key_env_enabled,
|
||||
self.auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
self.auth_env_telemetry.provider_env_key_present,
|
||||
self.auth_env_telemetry.refresh_token_url_override_present,
|
||||
debug.request_id.as_deref(),
|
||||
debug.cf_ray.as_deref(),
|
||||
debug.auth_error.as_deref(),
|
||||
debug.auth_error_code.as_deref(),
|
||||
debug.error_body_class,
|
||||
debug.safe_error_message,
|
||||
);
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
endpoint: self.request_route_telemetry.endpoint,
|
||||
auth_header_attached: self.auth_context.auth_header_attached,
|
||||
@@ -1876,23 +1825,19 @@ impl RequestTelemetry for ApiTelemetry {
|
||||
auth_recovery_mode: self.auth_context.recovery_mode,
|
||||
auth_recovery_phase: self.auth_context.recovery_phase,
|
||||
auth_connection_reused: None,
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names: self.provider_header_names.as_deref(),
|
||||
base_url_origin: self.endpoint_telemetry.base_url_origin,
|
||||
host_class: self.endpoint_telemetry.host_class,
|
||||
base_url_source: self.endpoint_telemetry.base_url_source,
|
||||
base_url_is_default: self.endpoint_telemetry.base_url_is_default,
|
||||
residency_header_attached: Some(self.request_route_telemetry.residency_header_attached),
|
||||
residency_header_value: self
|
||||
.request_route_telemetry
|
||||
.residency_header_value
|
||||
.as_deref(),
|
||||
auth_request_id: debug.request_id.as_deref(),
|
||||
auth_cf_ray: debug.cf_ray.as_deref(),
|
||||
auth_error: debug.auth_error.as_deref(),
|
||||
auth_error_code: debug.auth_error_code.as_deref(),
|
||||
error_body_class: debug.error_body_class,
|
||||
safe_error_message: debug.safe_error_message,
|
||||
geo_denial_detected: Some(debug.geo_denial_detected),
|
||||
auth_recovery_followup_success: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
@@ -1903,25 +1848,6 @@ impl RequestTelemetry for ApiTelemetry {
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
});
|
||||
if status == Some(StatusCode::UNAUTHORIZED.as_u16()) && debug.geo_denial_detected {
|
||||
self.session_telemetry.record_geo_denial(
|
||||
self.request_route_telemetry.endpoint,
|
||||
self.auth_context.auth_header_attached,
|
||||
self.auth_context.auth_header_name,
|
||||
self.request_route_telemetry.residency_header_attached,
|
||||
self.request_route_telemetry
|
||||
.residency_header_value
|
||||
.as_deref(),
|
||||
self.provider_header_names.as_deref(),
|
||||
status,
|
||||
debug.request_id.as_deref(),
|
||||
debug.cf_ray.as_deref(),
|
||||
debug.auth_error.as_deref(),
|
||||
debug.auth_error_code.as_deref(),
|
||||
debug.error_body_class.unwrap_or_default(),
|
||||
debug.safe_error_message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1967,23 +1893,19 @@ impl WebsocketTelemetry for ApiTelemetry {
|
||||
auth_recovery_mode: self.auth_context.recovery_mode,
|
||||
auth_recovery_phase: self.auth_context.recovery_phase,
|
||||
auth_connection_reused: Some(connection_reused),
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names: self.provider_header_names.as_deref(),
|
||||
base_url_origin: self.endpoint_telemetry.base_url_origin,
|
||||
host_class: self.endpoint_telemetry.host_class,
|
||||
base_url_source: self.endpoint_telemetry.base_url_source,
|
||||
base_url_is_default: self.endpoint_telemetry.base_url_is_default,
|
||||
residency_header_attached: Some(self.request_route_telemetry.residency_header_attached),
|
||||
residency_header_value: self
|
||||
.request_route_telemetry
|
||||
.residency_header_value
|
||||
.as_deref(),
|
||||
auth_request_id: debug.request_id.as_deref(),
|
||||
auth_cf_ray: debug.cf_ray.as_deref(),
|
||||
auth_error: debug.auth_error.as_deref(),
|
||||
auth_error_code: debug.auth_error_code.as_deref(),
|
||||
error_body_class: debug.error_body_class,
|
||||
safe_error_message: debug.safe_error_message,
|
||||
geo_denial_detected: Some(debug.geo_denial_detected),
|
||||
auth_recovery_followup_success: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::compact::should_use_remote_compact_task;
|
||||
use crate::compact_remote::run_inline_remote_auto_compact_task;
|
||||
use crate::config::ManagedFeatures;
|
||||
use crate::connectors;
|
||||
use crate::endpoint_config_telemetry::resolve_endpoint_config_telemetry_source;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::features::FEATURES;
|
||||
use crate::features::Feature;
|
||||
@@ -154,7 +153,6 @@ use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ModelProviderInfo;
|
||||
use crate::auth_env_telemetry::collect_auth_env_telemetry;
|
||||
use crate::client::ModelClient;
|
||||
use crate::client::ModelClientSession;
|
||||
use crate::client_common::Prompt;
|
||||
@@ -1613,33 +1611,8 @@ impl Session {
|
||||
)],
|
||||
);
|
||||
|
||||
let auth_env_telemetry = collect_auth_env_telemetry(
|
||||
&config.model_provider,
|
||||
auth_manager.codex_api_key_env_enabled(),
|
||||
);
|
||||
let conversation_start_base_url = config
|
||||
.model_provider
|
||||
.to_api_provider(auth.map(CodexAuth::auth_mode))
|
||||
.map(|provider| provider.base_url)
|
||||
.unwrap_or_default();
|
||||
let endpoint_telemetry = resolve_endpoint_config_telemetry_source(
|
||||
&config,
|
||||
session_configuration.session_source.clone(),
|
||||
)
|
||||
.classify(&conversation_start_base_url);
|
||||
|
||||
session_telemetry.conversation_starts_with_endpoint_details(
|
||||
session_telemetry.conversation_starts(
|
||||
config.model_provider.name.as_str(),
|
||||
endpoint_telemetry.base_url_origin,
|
||||
endpoint_telemetry.host_class,
|
||||
endpoint_telemetry.base_url_source,
|
||||
endpoint_telemetry.base_url_is_default,
|
||||
auth_env_telemetry.openai_api_key_env_present,
|
||||
auth_env_telemetry.codex_api_key_env_present,
|
||||
auth_env_telemetry.codex_api_key_env_enabled,
|
||||
auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
auth_env_telemetry.provider_env_key_present,
|
||||
auth_env_telemetry.refresh_token_url_override_present,
|
||||
session_configuration.collaboration_mode.reasoning_effort(),
|
||||
config
|
||||
.model_reasoning_summary
|
||||
@@ -1823,14 +1796,10 @@ impl Session {
|
||||
network_proxy,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: state_db_ctx.clone(),
|
||||
model_client: ModelClient::new_with_endpoint_telemetry_source(
|
||||
model_client: ModelClient::new(
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
conversation_id,
|
||||
session_configuration.provider.clone(),
|
||||
resolve_endpoint_config_telemetry_source(
|
||||
&config,
|
||||
session_configuration.session_source.clone(),
|
||||
),
|
||||
session_configuration.session_source.clone(),
|
||||
config.model_verbosity,
|
||||
ws_version_from_features(config.as_ref()),
|
||||
|
||||
@@ -4136,7 +4136,7 @@ model_verbosity = "high"
|
||||
supports_websockets: false,
|
||||
};
|
||||
let model_provider_map = {
|
||||
let mut model_provider_map = built_in_model_providers(None);
|
||||
let mut model_provider_map = built_in_model_providers(/* openai_base_url */ None);
|
||||
model_provider_map.insert("openai-custom".to_string(), openai_custom_provider.clone());
|
||||
model_provider_map
|
||||
};
|
||||
|
||||
@@ -30,12 +30,6 @@ pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
|
||||
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
||||
pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency";
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct ResidencyHeaderTelemetry {
|
||||
pub attached: bool,
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Originator {
|
||||
pub value: String,
|
||||
@@ -95,33 +89,6 @@ pub fn set_default_client_residency_requirement(enforce_residency: Option<Reside
|
||||
*guard = enforce_residency;
|
||||
}
|
||||
|
||||
pub fn current_residency_header_telemetry() -> ResidencyHeaderTelemetry {
|
||||
let Ok(guard) = REQUIREMENTS_RESIDENCY.read() else {
|
||||
tracing::warn!("Failed to acquire requirements residency lock");
|
||||
return ResidencyHeaderTelemetry::default();
|
||||
};
|
||||
let Some(requirement) = guard.as_ref() else {
|
||||
return ResidencyHeaderTelemetry::default();
|
||||
};
|
||||
ResidencyHeaderTelemetry {
|
||||
attached: true,
|
||||
value: Some(residency_header_value(*requirement).to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn residency_header_telemetry_for_provider_headers(
|
||||
provider_headers: &HeaderMap,
|
||||
) -> ResidencyHeaderTelemetry {
|
||||
if let Some(value) = provider_headers.get(RESIDENCY_HEADER_NAME) {
|
||||
return ResidencyHeaderTelemetry {
|
||||
attached: true,
|
||||
value: value.to_str().ok().map(str::to_owned),
|
||||
};
|
||||
}
|
||||
|
||||
current_residency_header_telemetry()
|
||||
}
|
||||
|
||||
pub fn originator() -> Originator {
|
||||
if let Ok(guard) = ORIGINATOR.read()
|
||||
&& let Some(originator) = guard.as_ref()
|
||||
@@ -153,6 +120,18 @@ pub fn is_first_party_chat_originator(originator_value: &str) -> bool {
|
||||
originator_value == "codex_atlas" || originator_value == "codex_chatgpt_desktop"
|
||||
}
|
||||
|
||||
pub fn client_origin_class(originator_value: &str) -> &'static str {
|
||||
if is_first_party_chat_originator(originator_value) {
|
||||
"first_party_chat"
|
||||
} else if originator_value == DEFAULT_ORIGINATOR {
|
||||
"codex_cli"
|
||||
} else if is_first_party_originator(originator_value) {
|
||||
"first_party_ide"
|
||||
} else {
|
||||
"custom"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_codex_user_agent() -> String {
|
||||
let build_version = env!("CARGO_PKG_VERSION");
|
||||
let os_info = os_info::get();
|
||||
|
||||
@@ -204,133 +204,3 @@ fn classify_base_url(base_url: &str) -> (&'static str, &'static str) {
|
||||
fn is_chatgpt_codex_path(path: &str) -> bool {
|
||||
path == "/backend-api/codex" || path.starts_with("/backend-api/codex/")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::EndpointConfigTelemetry;
|
||||
use super::EndpointConfigTelemetrySource;
|
||||
use super::endpoint_source_from_layer;
|
||||
use super::endpoint_source_from_provider_defaults;
|
||||
use crate::model_provider_info::WireApi;
|
||||
use crate::model_provider_info::create_oss_provider_with_base_url;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn provider(base_url: Option<&str>) -> crate::ModelProviderInfo {
|
||||
crate::ModelProviderInfo {
|
||||
name: "test-provider".to_string(),
|
||||
base_url: base_url.map(str::to_string),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
wire_api: crate::WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
requires_openai_auth: true,
|
||||
supports_websockets: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_config_telemetry_classifies_known_hosts_without_logging_custom_values() {
|
||||
let source = EndpointConfigTelemetrySource::new("config_toml", false);
|
||||
|
||||
assert_eq!(
|
||||
source.classify("https://chatgpt.com/backend-api/codex"),
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "config_toml",
|
||||
base_url_is_default: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
source.classify("https://api.openai.com/v1"),
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin: "api.openai.com",
|
||||
host_class: "openai_api",
|
||||
base_url_source: "config_toml",
|
||||
base_url_is_default: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
source.classify("https://openrouter.ai/api/v1"),
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin: "openrouter.ai",
|
||||
host_class: "known_third_party",
|
||||
base_url_source: "config_toml",
|
||||
base_url_is_default: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
source.classify("https://private.example.internal/v1?token=secret"),
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin: "custom",
|
||||
host_class: "custom_unknown",
|
||||
base_url_source: "config_toml",
|
||||
base_url_is_default: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
source.classify("https://chatgpt.com/api/codex"),
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "custom_unknown",
|
||||
base_url_source: "config_toml",
|
||||
base_url_is_default: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_config_telemetry_source_maps_layers_and_defaults() {
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&ConfigLayerSource::SessionFlags, SessionSource::VSCode),
|
||||
EndpointConfigTelemetrySource::new("ide_settings", false)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(
|
||||
&ConfigLayerSource::Project {
|
||||
dot_codex_folder: AbsolutePathBuf::try_from(std::path::PathBuf::from(
|
||||
"/tmp/project/.codex",
|
||||
))
|
||||
.expect("absolute path"),
|
||||
},
|
||||
SessionSource::Cli,
|
||||
),
|
||||
EndpointConfigTelemetrySource::new("config_toml", false)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_provider_defaults("openai", &provider(None)),
|
||||
EndpointConfigTelemetrySource::new("default", true)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_provider_defaults(
|
||||
"custom",
|
||||
&provider(Some("https://example.com/v1"))
|
||||
),
|
||||
EndpointConfigTelemetrySource::new("config_toml", false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_config_telemetry_source_requires_explicit_provider_id_for_builtin_oss_defaults() {
|
||||
let provider =
|
||||
create_oss_provider_with_base_url("http://localhost:1234/v1", WireApi::Responses);
|
||||
|
||||
assert_eq!(
|
||||
EndpointConfigTelemetrySource::for_provider("lmstudio", &provider),
|
||||
EndpointConfigTelemetrySource::new("default", true)
|
||||
);
|
||||
assert_eq!(
|
||||
EndpointConfigTelemetrySource::for_provider_without_id(&provider),
|
||||
EndpointConfigTelemetrySource::new("config_toml", false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ pub type CodexConversation = CodexThread;
|
||||
pub use analytics_client::AnalyticsEventsClient;
|
||||
pub use auth::AuthManager;
|
||||
pub use auth::CodexAuth;
|
||||
mod auth_env_telemetry;
|
||||
pub mod default_client;
|
||||
pub mod project_doc;
|
||||
mod rollout;
|
||||
|
||||
@@ -15,7 +15,6 @@ use http::header::HeaderValue;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
@@ -23,6 +22,7 @@ use std::time::Duration;
|
||||
const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000;
|
||||
const DEFAULT_STREAM_MAX_RETRIES: u64 = 5;
|
||||
const DEFAULT_REQUEST_MAX_RETRIES: u64 = 4;
|
||||
pub(crate) const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS: u64 = 15_000;
|
||||
/// Hard cap for user-configured `stream_max_retries`.
|
||||
const MAX_STREAM_MAX_RETRIES: u64 = 100;
|
||||
/// Hard cap for user-configured `request_max_retries`.
|
||||
@@ -113,6 +113,10 @@ pub struct ModelProviderInfo {
|
||||
/// the connection as lost.
|
||||
pub stream_idle_timeout_ms: Option<u64>,
|
||||
|
||||
/// Maximum time (in milliseconds) to wait for a websocket connection attempt before treating
|
||||
/// it as failed.
|
||||
pub websocket_connect_timeout_ms: Option<u64>,
|
||||
|
||||
/// Does this provider require an OpenAI API Key or ChatGPT login token? If true,
|
||||
/// user is presented with login screen on first run, and login preference and token/key
|
||||
/// are stored in auth.json. If false (which is the default), login screen is skipped,
|
||||
@@ -127,34 +131,17 @@ pub struct ModelProviderInfo {
|
||||
|
||||
impl ModelProviderInfo {
|
||||
pub(crate) fn telemetry_header_names(&self) -> Option<String> {
|
||||
let mut names = BTreeSet::new();
|
||||
|
||||
if let Some(headers) = &self.http_headers {
|
||||
for (name, value) in headers {
|
||||
if let (Ok(name), Ok(_value)) =
|
||||
(HeaderName::try_from(name), HeaderValue::try_from(value))
|
||||
{
|
||||
names.insert(name.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(env_headers) = &self.env_http_headers {
|
||||
for (header, env_var) in env_headers {
|
||||
if let Ok(value) = std::env::var(env_var)
|
||||
&& !value.trim().is_empty()
|
||||
&& let (Ok(name), Ok(_value)) =
|
||||
(HeaderName::try_from(header), HeaderValue::try_from(value))
|
||||
{
|
||||
names.insert(name.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if names.is_empty() {
|
||||
let headers = self.build_header_map().ok()?;
|
||||
if headers.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(names.into_iter().collect::<Vec<_>>().join(","))
|
||||
Some(
|
||||
headers
|
||||
.keys()
|
||||
.map(HeaderName::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +246,13 @@ impl ModelProviderInfo {
|
||||
.map(Duration::from_millis)
|
||||
.unwrap_or(Duration::from_millis(DEFAULT_STREAM_IDLE_TIMEOUT_MS))
|
||||
}
|
||||
|
||||
/// Effective timeout for websocket connect attempts.
|
||||
pub fn websocket_connect_timeout(&self) -> Duration {
|
||||
self.websocket_connect_timeout_ms
|
||||
.map(Duration::from_millis)
|
||||
.unwrap_or(Duration::from_millis(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS))
|
||||
}
|
||||
pub fn create_openai_provider(base_url: Option<String>) -> ModelProviderInfo {
|
||||
ModelProviderInfo {
|
||||
name: OPENAI_PROVIDER_NAME.into(),
|
||||
@@ -288,6 +282,7 @@ impl ModelProviderInfo {
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: true,
|
||||
supports_websockets: true,
|
||||
}
|
||||
@@ -363,6 +358,7 @@ pub fn create_oss_provider_with_base_url(base_url: &str, wire_api: WireApi) -> M
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: false,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn openai_auth_provider_defaults_to_api_before_login() {
|
||||
let provider = ModelProviderInfo::create_openai_provider(None);
|
||||
|
||||
let api_provider = provider.to_api_provider(None).unwrap();
|
||||
|
||||
assert_eq!(api_provider.base_url, "https://api.openai.com/v1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_ollama_model_provider_toml() {
|
||||
let azure_provider_toml = r#"
|
||||
|
||||
@@ -3,11 +3,11 @@ use crate::api_bridge::auth_provider_from_auth;
|
||||
use crate::api_bridge::map_api_error;
|
||||
use crate::auth::AuthManager;
|
||||
use crate::auth::AuthMode;
|
||||
use crate::auth::CodexAuth;
|
||||
use crate::auth_env_telemetry::AuthEnvTelemetry;
|
||||
use crate::auth_env_telemetry::collect_auth_env_telemetry;
|
||||
use crate::config::Config;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::residency_header_telemetry_for_provider_headers;
|
||||
use crate::endpoint_config_telemetry::EndpointConfigTelemetrySource;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CoreResult;
|
||||
@@ -50,8 +50,6 @@ struct ModelsRequestTelemetry {
|
||||
auth_header_attached: bool,
|
||||
auth_header_name: Option<&'static str>,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
residency_header_attached: bool,
|
||||
residency_header_value: Option<String>,
|
||||
provider_header_names: Option<String>,
|
||||
base_url_origin: &'static str,
|
||||
host_class: &'static str,
|
||||
@@ -92,8 +90,6 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
auth.env_provider_key_name = self.auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
auth.env_provider_key_present = self.auth_env_telemetry.provider_env_key_present,
|
||||
auth.env_refresh_token_url_override_present = self.auth_env_telemetry.refresh_token_url_override_present,
|
||||
residency_header_attached = self.residency_header_attached,
|
||||
residency_header_value = self.residency_header_value.as_deref(),
|
||||
provider_header_names = self.provider_header_names.as_deref(),
|
||||
base_url_origin = self.base_url_origin,
|
||||
host_class = self.host_class,
|
||||
@@ -125,8 +121,6 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
auth.env_provider_key_name = self.auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
auth.env_provider_key_present = self.auth_env_telemetry.provider_env_key_present,
|
||||
auth.env_refresh_token_url_override_present = self.auth_env_telemetry.refresh_token_url_override_present,
|
||||
residency_header_attached = self.residency_header_attached,
|
||||
residency_header_value = self.residency_header_value.as_deref(),
|
||||
provider_header_names = self.provider_header_names.as_deref(),
|
||||
base_url_origin = self.base_url_origin,
|
||||
host_class = self.host_class,
|
||||
@@ -157,88 +151,22 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
auth_recovery_mode: None,
|
||||
auth_recovery_phase: None,
|
||||
auth_connection_reused: None,
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names: self.provider_header_names.as_deref(),
|
||||
base_url_origin: self.base_url_origin,
|
||||
host_class: self.host_class,
|
||||
base_url_source: self.base_url_source,
|
||||
base_url_is_default: self.base_url_is_default,
|
||||
residency_header_attached: Some(self.residency_header_attached),
|
||||
residency_header_value: self.residency_header_value.as_deref(),
|
||||
auth_request_id: response_debug.request_id.as_deref(),
|
||||
auth_cf_ray: response_debug.cf_ray.as_deref(),
|
||||
auth_error: response_debug.auth_error.as_deref(),
|
||||
auth_error_code: response_debug.auth_error_code.as_deref(),
|
||||
error_body_class: response_debug.error_body_class,
|
||||
safe_error_message: response_debug.safe_error_message,
|
||||
geo_denial_detected: Some(response_debug.geo_denial_detected),
|
||||
auth_recovery_followup_success: None,
|
||||
auth_recovery_followup_status: None,
|
||||
});
|
||||
|
||||
if status == Some(http::StatusCode::UNAUTHORIZED.as_u16())
|
||||
&& response_debug.geo_denial_detected
|
||||
{
|
||||
tracing::event!(
|
||||
target: "codex_otel.log_only",
|
||||
tracing::Level::INFO,
|
||||
event.name = "codex.geo_denial",
|
||||
geo_denial_detected = true,
|
||||
request_id = response_debug.request_id.as_deref(),
|
||||
cf_ray = response_debug.cf_ray.as_deref(),
|
||||
endpoint = MODELS_ENDPOINT,
|
||||
auth.header_attached = self.auth_header_attached,
|
||||
auth.header_name = self.auth_header_name,
|
||||
auth.mode = self.auth_mode.as_deref(),
|
||||
auth.env_openai_api_key_present = self.auth_env_telemetry.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.auth_env_telemetry.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.auth_env_telemetry.codex_api_key_env_enabled,
|
||||
auth.env_provider_key_name = self.auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
auth.env_provider_key_present = self.auth_env_telemetry.provider_env_key_present,
|
||||
auth.env_refresh_token_url_override_present = self.auth_env_telemetry.refresh_token_url_override_present,
|
||||
residency_header_attached = self.residency_header_attached,
|
||||
residency_header_value = self.residency_header_value.as_deref(),
|
||||
provider_header_names = self.provider_header_names.as_deref(),
|
||||
base_url_origin = self.base_url_origin,
|
||||
host_class = self.host_class,
|
||||
base_url_source = self.base_url_source,
|
||||
base_url_is_default = self.base_url_is_default,
|
||||
http_status = status,
|
||||
auth.error = response_debug.auth_error.as_deref(),
|
||||
auth.error_code = response_debug.auth_error_code.as_deref(),
|
||||
error_body_class = response_debug.error_body_class.unwrap_or_default(),
|
||||
safe_error_message = response_debug.safe_error_message,
|
||||
);
|
||||
tracing::event!(
|
||||
target: "codex_otel.trace_safe",
|
||||
tracing::Level::INFO,
|
||||
event.name = "codex.geo_denial",
|
||||
geo_denial_detected = true,
|
||||
request_id = response_debug.request_id.as_deref(),
|
||||
cf_ray = response_debug.cf_ray.as_deref(),
|
||||
endpoint = MODELS_ENDPOINT,
|
||||
auth.header_attached = self.auth_header_attached,
|
||||
auth.header_name = self.auth_header_name,
|
||||
auth.mode = self.auth_mode.as_deref(),
|
||||
auth.env_openai_api_key_present = self.auth_env_telemetry.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.auth_env_telemetry.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.auth_env_telemetry.codex_api_key_env_enabled,
|
||||
auth.env_provider_key_name = self.auth_env_telemetry.provider_env_key_name.as_deref(),
|
||||
auth.env_provider_key_present = self.auth_env_telemetry.provider_env_key_present,
|
||||
auth.env_refresh_token_url_override_present = self.auth_env_telemetry.refresh_token_url_override_present,
|
||||
residency_header_attached = self.residency_header_attached,
|
||||
residency_header_value = self.residency_header_value.as_deref(),
|
||||
provider_header_names = self.provider_header_names.as_deref(),
|
||||
base_url_origin = self.base_url_origin,
|
||||
host_class = self.host_class,
|
||||
base_url_source = self.base_url_source,
|
||||
base_url_is_default = self.base_url_is_default,
|
||||
http_status = status,
|
||||
auth.error = response_debug.auth_error.as_deref(),
|
||||
auth.error_code = response_debug.auth_error_code.as_deref(),
|
||||
error_body_class = response_debug.error_body_class.unwrap_or_default(),
|
||||
safe_error_message = response_debug.safe_error_message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,24 +232,6 @@ impl ModelsManager {
|
||||
model_catalog: Option<ModelsResponse>,
|
||||
collaboration_modes_config: CollaborationModesConfig,
|
||||
provider: ModelProviderInfo,
|
||||
) -> Self {
|
||||
Self::new_with_provider_and_endpoint_telemetry_source(
|
||||
codex_home,
|
||||
auth_manager,
|
||||
model_catalog,
|
||||
collaboration_modes_config,
|
||||
provider.clone(),
|
||||
EndpointConfigTelemetrySource::for_provider(OPENAI_PROVIDER_ID, &provider),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_provider_and_endpoint_telemetry_source(
|
||||
codex_home: PathBuf,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
model_catalog: Option<ModelsResponse>,
|
||||
collaboration_modes_config: CollaborationModesConfig,
|
||||
provider: ModelProviderInfo,
|
||||
endpoint_telemetry_source: EndpointConfigTelemetrySource,
|
||||
) -> Self {
|
||||
let auth_env_telemetry =
|
||||
collect_auth_env_telemetry(&provider, auth_manager.codex_api_key_env_enabled());
|
||||
@@ -338,6 +248,8 @@ impl ModelsManager {
|
||||
Self::load_remote_models_from_file()
|
||||
.unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}"))
|
||||
});
|
||||
let endpoint_telemetry_source =
|
||||
EndpointConfigTelemetrySource::for_provider_without_id(&provider);
|
||||
Self {
|
||||
remote_models: RwLock::new(remote_models),
|
||||
catalog_mode,
|
||||
@@ -533,21 +445,18 @@ impl ModelsManager {
|
||||
let _timer =
|
||||
codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]);
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let auth_mode = self.auth_manager.auth_mode();
|
||||
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
|
||||
let api_provider = self.provider.to_api_provider(auth_mode)?;
|
||||
let api_auth = auth_provider_from_auth(auth.clone(), &self.provider)?;
|
||||
let transport = ReqwestTransport::new(build_reqwest_client());
|
||||
let endpoint_telemetry = self
|
||||
.endpoint_telemetry_source
|
||||
.classify(api_provider.base_url.as_str());
|
||||
let residency = residency_header_telemetry_for_provider_headers(&api_provider.headers);
|
||||
let request_telemetry: Arc<dyn RequestTelemetry> = Arc::new(ModelsRequestTelemetry {
|
||||
auth_mode: auth_mode.map(|mode| TelemetryAuthMode::from(mode).to_string()),
|
||||
auth_header_attached: api_auth.auth_header_attached(),
|
||||
auth_header_name: api_auth.auth_header_name(),
|
||||
auth_env_telemetry: self.auth_env_telemetry.clone(),
|
||||
residency_header_attached: residency.attached,
|
||||
residency_header_value: residency.value,
|
||||
provider_header_names: self.provider.telemetry_header_names(),
|
||||
base_url_origin: endpoint_telemetry.base_url_origin,
|
||||
host_class: endpoint_telemetry.host_class,
|
||||
|
||||
@@ -597,7 +597,7 @@ fn models_request_telemetry_emits_auth_env_feedback_tags_on_failure() {
|
||||
auth_mode: Some(TelemetryAuthMode::Chatgpt.to_string()),
|
||||
auth_header_attached: true,
|
||||
auth_header_name: Some("authorization"),
|
||||
auth_env: crate::auth_env_telemetry::AuthEnvTelemetry {
|
||||
auth_env_telemetry: crate::auth_env_telemetry::AuthEnvTelemetry {
|
||||
openai_api_key_env_present: false,
|
||||
codex_api_key_env_present: false,
|
||||
codex_api_key_env_enabled: false,
|
||||
@@ -605,6 +605,11 @@ fn models_request_telemetry_emits_auth_env_feedback_tags_on_failure() {
|
||||
provider_env_key_present: Some(false),
|
||||
refresh_token_url_override_present: false,
|
||||
},
|
||||
provider_header_names: None,
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: true,
|
||||
};
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-request-id", "req-models-401".parse().unwrap());
|
||||
|
||||
@@ -21,7 +21,6 @@ pub(crate) struct ResponseDebugContext {
|
||||
pub(crate) auth_error_code: Option<String>,
|
||||
pub(crate) safe_error_message: Option<&'static str>,
|
||||
pub(crate) error_body_class: Option<&'static str>,
|
||||
pub(crate) geo_denial_detected: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext {
|
||||
@@ -59,9 +58,6 @@ pub(crate) fn extract_response_debug_context(transport: &TransportError) -> Resp
|
||||
.as_deref()
|
||||
.and_then(allowlisted_error_body_message);
|
||||
context.error_body_class = error_body.as_deref().and_then(classify_error_body_message);
|
||||
context.geo_denial_detected = context.error_body_class
|
||||
== Some(WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS)
|
||||
|| context.auth_error_code.as_deref() == Some(WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS);
|
||||
|
||||
context
|
||||
}
|
||||
@@ -164,119 +160,3 @@ fn truncate_with_ellipsis(input: &str, max_bytes: usize) -> String {
|
||||
truncated.push_str(ellipsis);
|
||||
truncated
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ResponseDebugContext;
|
||||
use super::WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS;
|
||||
use super::extract_response_debug_context;
|
||||
use super::telemetry_api_error_message;
|
||||
use super::telemetry_transport_error_message;
|
||||
use codex_api::TransportError;
|
||||
use codex_api::error::ApiError;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::StatusCode;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn extract_response_debug_context_decodes_geo_denial_details() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-oai-request-id", HeaderValue::from_static("req-geo"));
|
||||
headers.insert("cf-ray", HeaderValue::from_static("ray-geo"));
|
||||
headers.insert(
|
||||
"x-error-json",
|
||||
HeaderValue::from_static(
|
||||
"eyJlcnJvciI6eyJjb2RlIjoid29ya3NwYWNlX25vdF9hdXRob3JpemVkX2luX3JlZ2lvbiJ9fQ==",
|
||||
),
|
||||
);
|
||||
|
||||
let context = extract_response_debug_context(&TransportError::Http {
|
||||
status: StatusCode::UNAUTHORIZED,
|
||||
url: Some("https://chatgpt.com/backend-api/codex/responses".to_string()),
|
||||
headers: Some(headers),
|
||||
body: Some(
|
||||
r#"{"error":{"message":"Workspace is not authorized in this region."},"status":401}"#
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
context,
|
||||
ResponseDebugContext {
|
||||
request_id: Some("req-geo".to_string()),
|
||||
cf_ray: Some("ray-geo".to_string()),
|
||||
auth_error: None,
|
||||
auth_error_code: Some("workspace_not_authorized_in_region".to_string()),
|
||||
safe_error_message: Some("Workspace is not authorized in this region."),
|
||||
error_body_class: Some(WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS),
|
||||
geo_denial_detected: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_response_debug_context_detects_geo_denial_from_error_code_without_body_message() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-oai-request-id", HeaderValue::from_static("req-geo-code"));
|
||||
headers.insert(
|
||||
"x-error-json",
|
||||
HeaderValue::from_static(
|
||||
"eyJlcnJvciI6eyJjb2RlIjoid29ya3NwYWNlX25vdF9hdXRob3JpemVkX2luX3JlZ2lvbiJ9fQ==",
|
||||
),
|
||||
);
|
||||
|
||||
let context = extract_response_debug_context(&TransportError::Http {
|
||||
status: StatusCode::UNAUTHORIZED,
|
||||
url: Some("https://chatgpt.com/backend-api/codex/responses".to_string()),
|
||||
headers: Some(headers),
|
||||
body: Some(String::new()),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
context,
|
||||
ResponseDebugContext {
|
||||
request_id: Some("req-geo-code".to_string()),
|
||||
cf_ray: None,
|
||||
auth_error: None,
|
||||
auth_error_code: Some("workspace_not_authorized_in_region".to_string()),
|
||||
safe_error_message: None,
|
||||
error_body_class: None,
|
||||
geo_denial_detected: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telemetry_error_messages_omit_http_bodies() {
|
||||
let transport = TransportError::Http {
|
||||
status: StatusCode::UNAUTHORIZED,
|
||||
url: Some("https://chatgpt.com/backend-api/codex/responses".to_string()),
|
||||
headers: None,
|
||||
body: Some(r#"{"error":{"message":"secret token leaked"}}"#.to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(telemetry_transport_error_message(&transport), "http 401");
|
||||
assert_eq!(
|
||||
telemetry_api_error_message(&ApiError::Transport(transport)),
|
||||
"http 401"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telemetry_error_messages_preserve_non_http_details() {
|
||||
let network = TransportError::Network("dns lookup failed".to_string());
|
||||
let build = TransportError::Build("invalid header value".to_string());
|
||||
let stream = ApiError::Stream("socket closed".to_string());
|
||||
|
||||
assert_eq!(
|
||||
telemetry_transport_error_message(&network),
|
||||
"dns lookup failed"
|
||||
);
|
||||
assert_eq!(
|
||||
telemetry_transport_error_message(&build),
|
||||
"invalid header value"
|
||||
);
|
||||
assert_eq!(telemetry_api_error_message(&stream), "socket closed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::AuthManager;
|
||||
use crate::CodexAuth;
|
||||
use crate::ModelProviderInfo;
|
||||
use crate::OPENAI_PROVIDER_ID;
|
||||
use crate::agent::AgentControl;
|
||||
use crate::codex::Codex;
|
||||
use crate::codex::CodexSpawnArgs;
|
||||
@@ -8,7 +9,6 @@ use crate::codex::CodexSpawnOk;
|
||||
use crate::codex::INITIAL_SUBMIT_ID;
|
||||
use crate::codex_thread::CodexThread;
|
||||
use crate::config::Config;
|
||||
use crate::endpoint_config_telemetry::resolve_endpoint_config_telemetry_source_for_provider;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::file_watcher::FileWatcher;
|
||||
@@ -47,8 +47,6 @@ use tokio::sync::RwLock;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::model_provider_info::OPENAI_PROVIDER_ID;
|
||||
|
||||
const THREAD_CREATED_CHANNEL_CAPACITY: usize = 1024;
|
||||
/// Test-only override for enabling thread-manager behaviors used by integration
|
||||
/// tests.
|
||||
@@ -175,14 +173,7 @@ impl ThreadManager {
|
||||
.model_providers
|
||||
.get(OPENAI_PROVIDER_ID)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ModelProviderInfo::create_openai_provider(None));
|
||||
let openai_endpoint_telemetry_source =
|
||||
resolve_endpoint_config_telemetry_source_for_provider(
|
||||
config,
|
||||
OPENAI_PROVIDER_ID,
|
||||
&openai_models_provider,
|
||||
session_source.clone(),
|
||||
);
|
||||
.unwrap_or_else(|| ModelProviderInfo::create_openai_provider(/*base_url*/ None));
|
||||
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
|
||||
let plugins_manager = Arc::new(PluginsManager::new(codex_home.clone()));
|
||||
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
||||
@@ -196,16 +187,13 @@ impl ThreadManager {
|
||||
state: Arc::new(ThreadManagerState {
|
||||
threads: Arc::new(RwLock::new(HashMap::new())),
|
||||
thread_created_tx,
|
||||
models_manager: Arc::new(
|
||||
ModelsManager::new_with_provider_and_endpoint_telemetry_source(
|
||||
codex_home,
|
||||
auth_manager.clone(),
|
||||
config.model_catalog.clone(),
|
||||
collaboration_modes_config,
|
||||
openai_models_provider,
|
||||
openai_endpoint_telemetry_source,
|
||||
),
|
||||
),
|
||||
models_manager: Arc::new(ModelsManager::new_with_provider(
|
||||
codex_home,
|
||||
auth_manager.clone(),
|
||||
config.model_catalog.clone(),
|
||||
collaboration_modes_config,
|
||||
openai_models_provider,
|
||||
)),
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
@@ -225,7 +213,7 @@ impl ThreadManager {
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
) -> Self {
|
||||
set_thread_manager_test_mode_for_tests(true);
|
||||
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
|
||||
let codex_home = std::env::temp_dir().join(format!(
|
||||
"codex-thread-manager-test-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
@@ -245,7 +233,7 @@ impl ThreadManager {
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
) -> Self {
|
||||
set_thread_manager_test_mode_for_tests(true);
|
||||
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
|
||||
let plugins_manager = Arc::new(PluginsManager::new(codex_home.clone()));
|
||||
@@ -253,7 +241,7 @@ impl ThreadManager {
|
||||
let skills_manager = Arc::new(SkillsManager::new(
|
||||
codex_home.clone(),
|
||||
Arc::clone(&plugins_manager),
|
||||
true,
|
||||
/*bundled_skills_enabled*/ true,
|
||||
));
|
||||
let file_watcher = build_file_watcher(codex_home.clone(), Arc::clone(&skills_manager));
|
||||
Self {
|
||||
@@ -352,7 +340,12 @@ impl ThreadManager {
|
||||
pub async fn start_thread(&self, config: Config) -> CodexResult<NewThread> {
|
||||
// Box delegated thread-spawn futures so these convenience wrappers do
|
||||
// not inline the full spawn path into every caller's async state.
|
||||
Box::pin(self.start_thread_with_tools(config, Vec::new(), false)).await
|
||||
Box::pin(self.start_thread_with_tools(
|
||||
config,
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start_thread_with_tools(
|
||||
@@ -365,8 +358,8 @@ impl ThreadManager {
|
||||
config,
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
None,
|
||||
None,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -388,6 +381,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
parent_trace,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -404,7 +398,7 @@ impl ThreadManager {
|
||||
config,
|
||||
initial_history,
|
||||
auth_manager,
|
||||
false,
|
||||
/*persist_extended_history*/ false,
|
||||
parent_trace,
|
||||
))
|
||||
.await
|
||||
@@ -425,8 +419,50 @@ impl ThreadManager {
|
||||
self.agent_control(),
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
None,
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn start_thread_with_user_shell_override_for_tests(
|
||||
&self,
|
||||
config: Config,
|
||||
user_shell_override: crate::shell::Shell,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.state.spawn_thread(
|
||||
config,
|
||||
InitialHistory::New,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn resume_thread_from_rollout_with_user_shell_override_for_tests(
|
||||
&self,
|
||||
config: Config,
|
||||
rollout_path: PathBuf,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
user_shell_override: crate::shell::Shell,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
|
||||
Box::pin(self.state.spawn_thread(
|
||||
config,
|
||||
initial_history,
|
||||
auth_manager,
|
||||
self.agent_control(),
|
||||
Vec::new(),
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -510,8 +546,9 @@ impl ThreadManager {
|
||||
self.agent_control(),
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
None,
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -570,9 +607,9 @@ impl ThreadManagerState {
|
||||
config,
|
||||
agent_control,
|
||||
self.session_source.clone(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*inherited_shell_snapshot*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -596,7 +633,8 @@ impl ThreadManagerState {
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
inherited_shell_snapshot,
|
||||
None,
|
||||
/*parent_trace*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -617,10 +655,11 @@ impl ThreadManagerState {
|
||||
agent_control,
|
||||
session_source,
|
||||
Vec::new(),
|
||||
false,
|
||||
None,
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
inherited_shell_snapshot,
|
||||
None,
|
||||
/*parent_trace*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -642,9 +681,10 @@ impl ThreadManagerState {
|
||||
session_source,
|
||||
Vec::new(),
|
||||
persist_extended_history,
|
||||
None,
|
||||
/*metrics_service_name*/ None,
|
||||
inherited_shell_snapshot,
|
||||
None,
|
||||
/*parent_trace*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -661,6 +701,7 @@ impl ThreadManagerState {
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.spawn_thread_with_source(
|
||||
config,
|
||||
@@ -671,8 +712,9 @@ impl ThreadManagerState {
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
None,
|
||||
/*inherited_shell_snapshot*/ None,
|
||||
parent_trace,
|
||||
user_shell_override,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -690,6 +732,7 @@ impl ThreadManagerState {
|
||||
metrics_service_name: Option<String>,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let watch_registration = self
|
||||
.file_watcher
|
||||
@@ -711,6 +754,7 @@ impl ThreadManagerState {
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
inherited_shell_snapshot,
|
||||
user_shell_override,
|
||||
parent_trace,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -59,7 +59,7 @@ fn function_payload(args: serde_json::Value) -> ToolPayload {
|
||||
fn thread_manager() -> ThreadManager {
|
||||
ThreadManager::with_models_provider_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
built_in_model_providers(None)["openai"].clone(),
|
||||
built_in_model_providers(/* openai_base_url */ None)["openai"].clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let mut config = (*turn.config).clone();
|
||||
let provider = built_in_model_providers(None)["ollama"].clone();
|
||||
let provider = built_in_model_providers(/* openai_base_url */ None)["ollama"].clone();
|
||||
config.model_provider_id = "ollama".to_string();
|
||||
config.model_provider = provider.clone();
|
||||
config
|
||||
|
||||
+12
-13
@@ -7,6 +7,8 @@ use rand::Rng;
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
|
||||
use crate::default_client::client_origin_class;
|
||||
use crate::default_client::originator;
|
||||
use crate::parse_command::shlex_join;
|
||||
|
||||
const INITIAL_DELAY_MS: u64 = 200;
|
||||
@@ -52,20 +54,19 @@ pub(crate) struct FeedbackRequestTags<'a> {
|
||||
pub auth_recovery_mode: Option<&'a str>,
|
||||
pub auth_recovery_phase: Option<&'a str>,
|
||||
pub auth_connection_reused: Option<bool>,
|
||||
pub app_server_auth_state: Option<&'a str>,
|
||||
pub app_server_requires_openai_auth: Option<bool>,
|
||||
pub provider_header_names: Option<&'a str>,
|
||||
pub base_url_origin: &'a str,
|
||||
pub host_class: &'a str,
|
||||
pub base_url_source: &'a str,
|
||||
pub base_url_is_default: bool,
|
||||
pub residency_header_attached: Option<bool>,
|
||||
pub residency_header_value: Option<&'a str>,
|
||||
pub auth_request_id: Option<&'a str>,
|
||||
pub auth_cf_ray: Option<&'a str>,
|
||||
pub auth_error: Option<&'a str>,
|
||||
pub auth_error_code: Option<&'a str>,
|
||||
pub error_body_class: Option<&'a str>,
|
||||
pub safe_error_message: Option<&'a str>,
|
||||
pub geo_denial_detected: Option<bool>,
|
||||
pub auth_recovery_followup_success: Option<bool>,
|
||||
pub auth_recovery_followup_status: Option<u16>,
|
||||
}
|
||||
@@ -108,28 +109,27 @@ pub(crate) fn emit_feedback_request_tags(tags: &FeedbackRequestTags<'_>) {
|
||||
let auth_connection_reused = tags
|
||||
.auth_connection_reused
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let provider_header_names = tags.provider_header_names.unwrap_or("");
|
||||
let residency_header_attached = tags
|
||||
.residency_header_attached
|
||||
let app_server_auth_state = tags.app_server_auth_state.unwrap_or("");
|
||||
let app_server_requires_openai_auth = tags
|
||||
.app_server_requires_openai_auth
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let residency_header_value = tags.residency_header_value.unwrap_or("");
|
||||
let provider_header_names = tags.provider_header_names.unwrap_or("");
|
||||
let auth_request_id = tags.auth_request_id.unwrap_or("");
|
||||
let auth_cf_ray = tags.auth_cf_ray.unwrap_or("");
|
||||
let auth_error = tags.auth_error.unwrap_or("");
|
||||
let auth_error_code = tags.auth_error_code.unwrap_or("");
|
||||
let error_body_class = tags.error_body_class.unwrap_or("");
|
||||
let safe_error_message = tags.safe_error_message.unwrap_or("");
|
||||
let geo_denial_detected = tags
|
||||
.geo_denial_detected
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let auth_recovery_followup_success = tags
|
||||
.auth_recovery_followup_success
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let auth_recovery_followup_status = tags
|
||||
.auth_recovery_followup_status
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let originator = originator().value;
|
||||
feedback_tags!(
|
||||
endpoint = tags.endpoint,
|
||||
client_origin = client_origin_class(originator.as_str()),
|
||||
auth_header_attached = tags.auth_header_attached,
|
||||
auth_header_name = auth_header_name,
|
||||
auth_mode = auth_mode,
|
||||
@@ -144,20 +144,19 @@ pub(crate) fn emit_feedback_request_tags(tags: &FeedbackRequestTags<'_>) {
|
||||
auth_recovery_mode = auth_recovery_mode,
|
||||
auth_recovery_phase = auth_recovery_phase,
|
||||
auth_connection_reused = auth_connection_reused,
|
||||
app_server_auth_state = app_server_auth_state,
|
||||
app_server_requires_openai_auth = app_server_requires_openai_auth,
|
||||
provider_header_names = provider_header_names,
|
||||
base_url_origin = tags.base_url_origin,
|
||||
host_class = tags.host_class,
|
||||
base_url_source = tags.base_url_source,
|
||||
base_url_is_default = tags.base_url_is_default,
|
||||
residency_header_attached = residency_header_attached,
|
||||
residency_header_value = residency_header_value,
|
||||
auth_request_id = auth_request_id,
|
||||
auth_cf_ray = auth_cf_ray,
|
||||
auth_error = auth_error,
|
||||
auth_error_code = auth_error_code,
|
||||
error_body_class = error_body_class,
|
||||
safe_error_message = safe_error_message,
|
||||
geo_denial_detected = geo_denial_detected,
|
||||
auth_recovery_followup_success = auth_recovery_followup_success,
|
||||
auth_recovery_followup_status = auth_recovery_followup_status
|
||||
);
|
||||
|
||||
+170
-106
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::auth_env_telemetry::AuthEnvTelemetry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -68,6 +69,7 @@ impl Visit for TagCollectorVisitor {
|
||||
#[derive(Clone)]
|
||||
struct TagCollectorLayer {
|
||||
tags: Arc<Mutex<BTreeMap<String, String>>>,
|
||||
event_count: Arc<Mutex<usize>>,
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for TagCollectorLayer
|
||||
@@ -81,48 +83,49 @@ where
|
||||
let mut visitor = TagCollectorVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
self.tags.lock().unwrap().extend(visitor.tags);
|
||||
*self.event_count.lock().unwrap() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_feedback_request_tags_records_sentry_feedback_fields() {
|
||||
let tags = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
let event_count = Arc::new(Mutex::new(0));
|
||||
let _guard = tracing_subscriber::registry()
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.with(TagCollectorLayer {
|
||||
tags: tags.clone(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.set_default();
|
||||
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
endpoint: "/responses",
|
||||
auth_header_attached: true,
|
||||
auth_header_name: Some("authorization"),
|
||||
auth_mode: Some("chatgpt"),
|
||||
auth_env_openai_api_key_present: true,
|
||||
auth_env_codex_api_key_present: false,
|
||||
auth_env_codex_api_key_enabled: true,
|
||||
auth_env_provider_key_name: Some("OPENAI_API_KEY"),
|
||||
auth_env_provider_key_present: Some(true),
|
||||
auth_env_refresh_token_url_override_present: true,
|
||||
auth_retry_after_unauthorized: Some(false),
|
||||
auth_recovery_mode: Some("managed"),
|
||||
auth_recovery_phase: Some("refresh_token"),
|
||||
auth_connection_reused: Some(true),
|
||||
provider_header_names: Some("openai-project"),
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: true,
|
||||
residency_header_attached: Some(true),
|
||||
residency_header_value: Some("us"),
|
||||
auth_request_id: Some("req-123"),
|
||||
auth_cf_ray: Some("ray-123"),
|
||||
auth_error: Some("missing_authorization_header"),
|
||||
auth_error_code: Some("token_expired"),
|
||||
error_body_class: Some("workspace_not_authorized_in_region"),
|
||||
safe_error_message: Some("Workspace is not authorized in this region."),
|
||||
geo_denial_detected: Some(true),
|
||||
auth_recovery_followup_success: Some(true),
|
||||
auth_recovery_followup_status: Some(200),
|
||||
});
|
||||
let auth_env = AuthEnvTelemetry {
|
||||
openai_api_key_env_present: true,
|
||||
codex_api_key_env_present: false,
|
||||
codex_api_key_env_enabled: true,
|
||||
provider_env_key_name: Some("configured".to_string()),
|
||||
provider_env_key_present: Some(true),
|
||||
refresh_token_url_override_present: true,
|
||||
};
|
||||
|
||||
emit_feedback_request_tags_with_auth_env(
|
||||
&FeedbackRequestTags {
|
||||
endpoint: "/responses",
|
||||
auth_header_attached: true,
|
||||
auth_header_name: Some("authorization"),
|
||||
auth_mode: Some("chatgpt"),
|
||||
auth_retry_after_unauthorized: Some(false),
|
||||
auth_recovery_mode: Some("managed"),
|
||||
auth_recovery_phase: Some("refresh_token"),
|
||||
auth_connection_reused: Some(true),
|
||||
auth_request_id: Some("req-123"),
|
||||
auth_cf_ray: Some("ray-123"),
|
||||
auth_error: Some("missing_authorization_header"),
|
||||
auth_error_code: Some("token_expired"),
|
||||
auth_recovery_followup_success: Some(true),
|
||||
auth_recovery_followup_status: Some(200),
|
||||
},
|
||||
&auth_env,
|
||||
);
|
||||
|
||||
let tags = tags.lock().unwrap().clone();
|
||||
assert_eq!(
|
||||
@@ -138,12 +141,23 @@ fn emit_feedback_request_tags_records_sentry_feedback_fields() {
|
||||
Some("\"authorization\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_request_id").map(String::as_str),
|
||||
Some("\"req-123\"")
|
||||
tags.get("auth_env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_codex_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("false")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_codex_api_key_enabled")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_name").map(String::as_str),
|
||||
Some("\"OPENAI_API_KEY\"")
|
||||
Some("\"configured\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_present")
|
||||
@@ -151,12 +165,17 @@ fn emit_feedback_request_tags_records_sentry_feedback_fields() {
|
||||
Some("\"true\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_error_code").map(String::as_str),
|
||||
Some("\"token_expired\"")
|
||||
tags.get("auth_env_refresh_token_url_override_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("geo_denial_detected").map(String::as_str),
|
||||
Some("\"true\"")
|
||||
tags.get("auth_request_id").map(String::as_str),
|
||||
Some("\"req-123\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_error_code").map(String::as_str),
|
||||
Some("\"token_expired\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_recovery_followup_success")
|
||||
@@ -168,13 +187,18 @@ fn emit_feedback_request_tags_records_sentry_feedback_fields() {
|
||||
.map(String::as_str),
|
||||
Some("\"200\"")
|
||||
);
|
||||
assert_eq!(*event_count.lock().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_feedback_auth_recovery_tags_preserves_401_specific_fields() {
|
||||
let tags = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
let event_count = Arc::new(Mutex::new(0));
|
||||
let _guard = tracing_subscriber::registry()
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.with(TagCollectorLayer {
|
||||
tags: tags.clone(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.set_default();
|
||||
|
||||
emit_feedback_auth_recovery_tags(
|
||||
@@ -204,13 +228,18 @@ fn emit_feedback_auth_recovery_tags_preserves_401_specific_fields() {
|
||||
tags.get("auth_401_error_code").map(String::as_str),
|
||||
Some("\"token_expired\"")
|
||||
);
|
||||
assert_eq!(*event_count.lock().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_feedback_auth_recovery_tags_clears_stale_401_fields() {
|
||||
let tags = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
let event_count = Arc::new(Mutex::new(0));
|
||||
let _guard = tracing_subscriber::registry()
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.with(TagCollectorLayer {
|
||||
tags: tags.clone(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.set_default();
|
||||
|
||||
emit_feedback_auth_recovery_tags(
|
||||
@@ -246,13 +275,18 @@ fn emit_feedback_auth_recovery_tags_clears_stale_401_fields() {
|
||||
tags.get("auth_401_error_code").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(*event_count.lock().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_feedback_request_tags_clears_stale_latest_auth_fields() {
|
||||
fn emit_feedback_request_tags_preserves_latest_auth_fields_after_unauthorized() {
|
||||
let tags = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
let event_count = Arc::new(Mutex::new(0));
|
||||
let _guard = tracing_subscriber::registry()
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.with(TagCollectorLayer {
|
||||
tags: tags.clone(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.set_default();
|
||||
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
@@ -260,62 +294,95 @@ fn emit_feedback_request_tags_clears_stale_latest_auth_fields() {
|
||||
auth_header_attached: true,
|
||||
auth_header_name: Some("authorization"),
|
||||
auth_mode: Some("chatgpt"),
|
||||
auth_env_openai_api_key_present: true,
|
||||
auth_env_codex_api_key_present: true,
|
||||
auth_env_codex_api_key_enabled: true,
|
||||
auth_env_provider_key_name: Some("OPENAI_API_KEY"),
|
||||
auth_env_provider_key_present: Some(true),
|
||||
auth_env_refresh_token_url_override_present: true,
|
||||
auth_retry_after_unauthorized: Some(true),
|
||||
auth_recovery_mode: Some("managed"),
|
||||
auth_recovery_phase: Some("refresh_token"),
|
||||
auth_connection_reused: Some(true),
|
||||
provider_header_names: Some("openai-project"),
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: true,
|
||||
residency_header_attached: Some(true),
|
||||
residency_header_value: Some("us"),
|
||||
auth_connection_reused: None,
|
||||
auth_request_id: Some("req-123"),
|
||||
auth_cf_ray: Some("ray-123"),
|
||||
auth_error: Some("missing_authorization_header"),
|
||||
auth_error_code: Some("token_expired"),
|
||||
error_body_class: Some("workspace_not_authorized_in_region"),
|
||||
safe_error_message: Some("Workspace is not authorized in this region."),
|
||||
geo_denial_detected: Some(true),
|
||||
auth_recovery_followup_success: Some(true),
|
||||
auth_recovery_followup_status: Some(200),
|
||||
auth_recovery_followup_success: Some(false),
|
||||
auth_recovery_followup_status: Some(401),
|
||||
});
|
||||
|
||||
let tags = tags.lock().unwrap().clone();
|
||||
assert_eq!(
|
||||
tags.get("auth_request_id").map(String::as_str),
|
||||
Some("\"req-123\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_cf_ray").map(String::as_str),
|
||||
Some("\"ray-123\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_error").map(String::as_str),
|
||||
Some("\"missing_authorization_header\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_error_code").map(String::as_str),
|
||||
Some("\"token_expired\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_recovery_followup_success")
|
||||
.map(String::as_str),
|
||||
Some("\"false\"")
|
||||
);
|
||||
assert_eq!(*event_count.lock().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_feedback_request_tags_preserves_auth_env_fields_for_legacy_emitters() {
|
||||
let tags = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
let event_count = Arc::new(Mutex::new(0));
|
||||
let _guard = tracing_subscriber::registry()
|
||||
.with(TagCollectorLayer {
|
||||
tags: tags.clone(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.set_default();
|
||||
|
||||
let auth_env = AuthEnvTelemetry {
|
||||
openai_api_key_env_present: true,
|
||||
codex_api_key_env_present: true,
|
||||
codex_api_key_env_enabled: true,
|
||||
provider_env_key_name: Some("configured".to_string()),
|
||||
provider_env_key_present: Some(true),
|
||||
refresh_token_url_override_present: true,
|
||||
};
|
||||
|
||||
emit_feedback_request_tags_with_auth_env(
|
||||
&FeedbackRequestTags {
|
||||
endpoint: "/responses",
|
||||
auth_header_attached: true,
|
||||
auth_header_name: Some("authorization"),
|
||||
auth_mode: Some("chatgpt"),
|
||||
auth_retry_after_unauthorized: Some(false),
|
||||
auth_recovery_mode: Some("managed"),
|
||||
auth_recovery_phase: Some("refresh_token"),
|
||||
auth_connection_reused: Some(true),
|
||||
auth_request_id: Some("req-123"),
|
||||
auth_cf_ray: Some("ray-123"),
|
||||
auth_error: Some("missing_authorization_header"),
|
||||
auth_error_code: Some("token_expired"),
|
||||
auth_recovery_followup_success: Some(true),
|
||||
auth_recovery_followup_status: Some(200),
|
||||
},
|
||||
&auth_env,
|
||||
);
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
endpoint: "/responses",
|
||||
auth_header_attached: true,
|
||||
auth_header_name: None,
|
||||
auth_mode: None,
|
||||
auth_env_openai_api_key_present: false,
|
||||
auth_env_codex_api_key_present: false,
|
||||
auth_env_codex_api_key_enabled: false,
|
||||
auth_env_provider_key_name: None,
|
||||
auth_env_provider_key_present: None,
|
||||
auth_env_refresh_token_url_override_present: false,
|
||||
auth_retry_after_unauthorized: None,
|
||||
auth_recovery_mode: None,
|
||||
auth_recovery_phase: None,
|
||||
auth_connection_reused: None,
|
||||
provider_header_names: None,
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: true,
|
||||
residency_header_attached: None,
|
||||
residency_header_value: None,
|
||||
auth_request_id: None,
|
||||
auth_cf_ray: None,
|
||||
auth_error: None,
|
||||
auth_error_code: None,
|
||||
error_body_class: None,
|
||||
safe_error_message: None,
|
||||
geo_denial_detected: None,
|
||||
auth_recovery_followup_success: None,
|
||||
auth_recovery_followup_status: None,
|
||||
});
|
||||
@@ -326,27 +393,6 @@ fn emit_feedback_request_tags_clears_stale_latest_auth_fields() {
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(tags.get("auth_mode").map(String::as_str), Some("\"\""));
|
||||
assert_eq!(
|
||||
tags.get("provider_header_names").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_name").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_present")
|
||||
.map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("residency_header_attached").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("residency_header_value").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_request_id").map(String::as_str),
|
||||
Some("\"\"")
|
||||
@@ -358,16 +404,33 @@ fn emit_feedback_request_tags_clears_stale_latest_auth_fields() {
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("error_body_class").map(String::as_str),
|
||||
Some("\"\"")
|
||||
tags.get("auth_env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("safe_error_message").map(String::as_str),
|
||||
Some("\"\"")
|
||||
tags.get("auth_env_codex_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("geo_denial_detected").map(String::as_str),
|
||||
Some("\"\"")
|
||||
tags.get("auth_env_codex_api_key_enabled")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_name").map(String::as_str),
|
||||
Some("\"configured\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_present")
|
||||
.map(String::as_str),
|
||||
Some("\"true\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_refresh_token_url_override_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_recovery_followup_success")
|
||||
@@ -379,6 +442,7 @@ fn emit_feedback_request_tags_clears_stale_latest_auth_fields() {
|
||||
.map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(*event_count.lock().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,6 +13,8 @@ use codex_core::built_in_model_providers;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_core::shell::Shell;
|
||||
use codex_core::shell::get_shell_by_model_provided_path;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -64,6 +66,7 @@ pub struct TestCodexBuilder {
|
||||
auth: CodexAuth,
|
||||
pre_build_hooks: Vec<Box<PreBuildHook>>,
|
||||
home: Option<Arc<TempDir>>,
|
||||
user_shell_override: Option<Shell>,
|
||||
}
|
||||
|
||||
impl TestCodexBuilder {
|
||||
@@ -100,12 +103,25 @@ impl TestCodexBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_user_shell(mut self, user_shell: Shell) -> Self {
|
||||
self.user_shell_override = Some(user_shell);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_windows_cmd_shell(self) -> Self {
|
||||
if cfg!(windows) {
|
||||
self.with_user_shell(get_shell_by_model_provided_path(&PathBuf::from("cmd.exe")))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build(&mut self, server: &wiremock::MockServer) -> anyhow::Result<TestCodex> {
|
||||
let home = match self.home.clone() {
|
||||
Some(home) => home,
|
||||
None => Arc::new(TempDir::new()?),
|
||||
};
|
||||
Box::pin(self.build_with_home(server, home, None)).await
|
||||
Box::pin(self.build_with_home(server, home, /*resume_from*/ None)).await
|
||||
}
|
||||
|
||||
pub async fn build_with_streaming_server(
|
||||
@@ -117,7 +133,12 @@ impl TestCodexBuilder {
|
||||
Some(home) => home,
|
||||
None => Arc::new(TempDir::new()?),
|
||||
};
|
||||
Box::pin(self.build_with_home_and_base_url(format!("{base_url}/v1"), home, None)).await
|
||||
Box::pin(self.build_with_home_and_base_url(
|
||||
format!("{base_url}/v1"),
|
||||
home,
|
||||
/*resume_from*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn build_with_websocket_server(
|
||||
@@ -138,7 +159,7 @@ impl TestCodexBuilder {
|
||||
.enable(Feature::ResponsesWebsockets)
|
||||
.expect("test config should allow feature update");
|
||||
}));
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, None)).await
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None)).await
|
||||
}
|
||||
|
||||
pub async fn resume(
|
||||
@@ -194,19 +215,43 @@ impl TestCodexBuilder {
|
||||
)
|
||||
};
|
||||
let thread_manager = Arc::new(thread_manager);
|
||||
let user_shell_override = self.user_shell_override.clone();
|
||||
|
||||
let new_conversation = match resume_from {
|
||||
Some(path) => {
|
||||
let new_conversation = match (resume_from, user_shell_override) {
|
||||
(Some(path), Some(user_shell_override)) => {
|
||||
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
|
||||
Box::pin(
|
||||
codex_core::test_support::resume_thread_from_rollout_with_user_shell_override(
|
||||
thread_manager.as_ref(),
|
||||
config.clone(),
|
||||
path,
|
||||
auth_manager,
|
||||
user_shell_override,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
(Some(path), None) => {
|
||||
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
|
||||
Box::pin(thread_manager.resume_thread_from_rollout(
|
||||
config.clone(),
|
||||
path,
|
||||
auth_manager,
|
||||
None,
|
||||
/*parent_trace*/ None,
|
||||
))
|
||||
.await?
|
||||
}
|
||||
None => Box::pin(thread_manager.start_thread(config.clone())).await?,
|
||||
(None, Some(user_shell_override)) => {
|
||||
Box::pin(
|
||||
codex_core::test_support::start_thread_with_user_shell_override(
|
||||
thread_manager.as_ref(),
|
||||
config.clone(),
|
||||
user_shell_override,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
(None, None) => Box::pin(thread_manager.start_thread(config.clone())).await?,
|
||||
};
|
||||
|
||||
Ok(TestCodex {
|
||||
@@ -226,7 +271,7 @@ impl TestCodexBuilder {
|
||||
) -> anyhow::Result<(Config, Arc<TempDir>)> {
|
||||
let model_provider = ModelProviderInfo {
|
||||
base_url: Some(base_url),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/*openai_base_url*/ None)["openai"].clone()
|
||||
};
|
||||
let cwd = Arc::new(TempDir::new()?);
|
||||
let mut config = load_default_config_for_test(home).await;
|
||||
@@ -362,8 +407,13 @@ impl TestCodex {
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
) -> Result<()> {
|
||||
self.submit_turn_with_context(prompt, approval_policy, sandbox_policy, None)
|
||||
.await
|
||||
self.submit_turn_with_context(
|
||||
prompt,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
/*service_tier*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_turn_with_context(
|
||||
@@ -552,6 +602,7 @@ pub fn test_codex() -> TestCodexBuilder {
|
||||
auth: CodexAuth::from_api_key("dummy"),
|
||||
pre_build_hooks: vec![],
|
||||
home: None,
|
||||
user_shell_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -715,7 +715,7 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut model_provider = built_in_model_providers(None)["openai"].clone();
|
||||
let mut model_provider = built_in_model_providers(/* openai_base_url */ None)["openai"].clone();
|
||||
model_provider.base_url = Some(format!("{}/api/codex", server.uri()));
|
||||
let mut builder = test_codex()
|
||||
.with_auth(create_dummy_codex_auth())
|
||||
@@ -791,7 +791,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
|
||||
let model_provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
|
||||
// Init session
|
||||
@@ -1966,7 +1966,7 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut provider = built_in_model_providers(None)["openai"].clone();
|
||||
let mut provider = built_in_model_providers(/* openai_base_url */ None)["openai"].clone();
|
||||
provider.base_url = Some(format!("{}/v1", server.uri()));
|
||||
|
||||
let mut builder = test_codex()
|
||||
|
||||
@@ -93,7 +93,7 @@ fn json_fragment(text: &str) -> String {
|
||||
}
|
||||
|
||||
fn non_openai_model_provider(server: &MockServer) -> ModelProviderInfo {
|
||||
let mut provider = built_in_model_providers(None)["openai"].clone();
|
||||
let mut provider = built_in_model_providers(/* openai_base_url */ None)["openai"].clone();
|
||||
provider.name = "OpenAI (test)".into();
|
||||
provider.base_url = Some(format!("{}/v1", server.uri()));
|
||||
provider
|
||||
|
||||
@@ -95,7 +95,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -654,7 +654,7 @@ async fn remote_models_do_not_append_removed_builtin_presets() -> Result<()> {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -709,7 +709,7 @@ async fn remote_models_merge_adds_new_high_priority_first() -> Result<()> {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -756,7 +756,7 @@ async fn remote_models_merge_replaces_overlapping_model() -> Result<()> {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -800,7 +800,7 @@ async fn remote_models_merge_preserves_bundled_models_on_empty_response() -> Res
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -841,7 +841,7 @@ async fn remote_models_request_times_out_after_5s() -> Result<()> {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
@@ -907,7 +907,7 @@ async fn remote_models_hide_picker_only_models() -> Result<()> {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers(None)["openai"].clone()
|
||||
..built_in_model_providers(/* openai_base_url */ None)["openai"].clone()
|
||||
};
|
||||
let manager = codex_core::test_support::models_manager_with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
|
||||
@@ -62,10 +62,33 @@ const RESPONSES_API_ENGINE_SERVICE_TTFT_FIELD: &str = "engine_service_ttft_total
|
||||
const RESPONSES_API_ENGINE_IAPI_TBT_FIELD: &str = "engine_iapi_tbt_across_engine_calls_ms";
|
||||
const RESPONSES_API_ENGINE_SERVICE_TBT_FIELD: &str = "engine_service_tbt_across_engine_calls_ms";
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AuthEnvTelemetryMetadata {
|
||||
pub openai_api_key_env_present: bool,
|
||||
pub codex_api_key_env_present: bool,
|
||||
pub codex_api_key_env_enabled: bool,
|
||||
pub provider_env_key_name: Option<String>,
|
||||
pub provider_env_key_present: Option<bool>,
|
||||
pub refresh_token_url_override_present: bool,
|
||||
}
|
||||
|
||||
fn client_origin_class(originator: &str) -> &'static str {
|
||||
if matches!(originator, "codex_atlas" | "codex_chatgpt_desktop") {
|
||||
"first_party_chat"
|
||||
} else if originator == "codex_cli_rs" {
|
||||
"codex_cli"
|
||||
} else if originator == "codex_vscode" || originator.starts_with("Codex ") {
|
||||
"first_party_ide"
|
||||
} else {
|
||||
"custom"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionTelemetryMetadata {
|
||||
pub(crate) conversation_id: ThreadId,
|
||||
pub(crate) auth_mode: Option<String>,
|
||||
pub(crate) auth_env: AuthEnvTelemetryMetadata,
|
||||
pub(crate) account_id: Option<String>,
|
||||
pub(crate) account_email: Option<String>,
|
||||
pub(crate) originator: String,
|
||||
@@ -86,6 +109,11 @@ pub struct SessionTelemetry {
|
||||
}
|
||||
|
||||
impl SessionTelemetry {
|
||||
pub fn with_auth_env(mut self, auth_env: AuthEnvTelemetryMetadata) -> Self {
|
||||
self.metadata.auth_env = auth_env;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_model(mut self, model: &str, slug: &str) -> Self {
|
||||
self.metadata.model = model.to_owned();
|
||||
self.metadata.slug = slug.to_owned();
|
||||
@@ -255,6 +283,7 @@ impl SessionTelemetry {
|
||||
metadata: SessionTelemetryMetadata {
|
||||
conversation_id,
|
||||
auth_mode: auth_mode.map(|m| m.to_string()),
|
||||
auth_env: AuthEnvTelemetryMetadata::default(),
|
||||
account_id,
|
||||
account_email,
|
||||
originator: sanitize_metric_tag_value(originator.as_str()),
|
||||
@@ -291,7 +320,6 @@ impl SessionTelemetry {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn conversation_starts(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
@@ -303,68 +331,12 @@ impl SessionTelemetry {
|
||||
sandbox_policy: SandboxPolicy,
|
||||
mcp_servers: Vec<&str>,
|
||||
active_profile: Option<String>,
|
||||
) {
|
||||
self.conversation_starts_with_endpoint_details(
|
||||
provider_name,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"unknown",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
context_window,
|
||||
auto_compact_token_limit,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
mcp_servers,
|
||||
active_profile,
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn conversation_starts_with_endpoint_details(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
base_url_origin: &str,
|
||||
host_class: &str,
|
||||
base_url_source: &str,
|
||||
base_url_is_default: bool,
|
||||
auth_env_openai_api_key_present: bool,
|
||||
auth_env_codex_api_key_present: bool,
|
||||
auth_env_codex_api_key_enabled: bool,
|
||||
auth_env_provider_key_name: Option<&str>,
|
||||
auth_env_provider_key_present: Option<bool>,
|
||||
auth_env_refresh_token_url_override_present: bool,
|
||||
reasoning_effort: Option<ReasoningEffort>,
|
||||
reasoning_summary: ReasoningSummary,
|
||||
context_window: Option<i64>,
|
||||
auto_compact_token_limit: Option<i64>,
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
mcp_servers: Vec<&str>,
|
||||
active_profile: Option<String>,
|
||||
) {
|
||||
log_and_trace_event!(
|
||||
self,
|
||||
common: {
|
||||
event.name = "codex.conversation_starts",
|
||||
provider_name = %provider_name,
|
||||
base_url_origin = base_url_origin,
|
||||
host_class = host_class,
|
||||
base_url_source = base_url_source,
|
||||
base_url_is_default = base_url_is_default,
|
||||
auth.env_openai_api_key_present = auth_env_openai_api_key_present,
|
||||
auth.env_codex_api_key_present = auth_env_codex_api_key_present,
|
||||
auth.env_codex_api_key_enabled = auth_env_codex_api_key_enabled,
|
||||
auth.env_provider_key_name = auth_env_provider_key_name,
|
||||
auth.env_provider_key_present = auth_env_provider_key_present,
|
||||
auth.env_refresh_token_url_override_present = auth_env_refresh_token_url_override_present,
|
||||
reasoning_effort = reasoning_effort.map(|e| e.to_string()),
|
||||
reasoning_summary = %reasoning_summary,
|
||||
context_window = context_window,
|
||||
@@ -396,7 +368,7 @@ impl SessionTelemetry {
|
||||
Ok(response) => (Some(response.status().as_u16()), None),
|
||||
Err(error) => (error.status().map(|s| s.as_u16()), Some(error.to_string())),
|
||||
};
|
||||
self.record_api_request_with_endpoint_details(
|
||||
self.record_api_request(
|
||||
attempt,
|
||||
status,
|
||||
error.as_deref(),
|
||||
@@ -407,8 +379,6 @@ impl SessionTelemetry {
|
||||
None,
|
||||
None,
|
||||
"unknown",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
"custom",
|
||||
"custom_unknown",
|
||||
@@ -431,7 +401,6 @@ impl SessionTelemetry {
|
||||
response
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn record_api_request(
|
||||
&self,
|
||||
attempt: u64,
|
||||
@@ -444,59 +413,6 @@ impl SessionTelemetry {
|
||||
recovery_mode: Option<&str>,
|
||||
recovery_phase: Option<&str>,
|
||||
endpoint: &str,
|
||||
request_id: Option<&str>,
|
||||
cf_ray: Option<&str>,
|
||||
auth_error: Option<&str>,
|
||||
auth_error_code: Option<&str>,
|
||||
) {
|
||||
self.record_api_request_with_endpoint_details(
|
||||
attempt,
|
||||
status,
|
||||
error,
|
||||
duration,
|
||||
auth_header_attached,
|
||||
auth_header_name,
|
||||
retry_after_unauthorized,
|
||||
recovery_mode,
|
||||
recovery_phase,
|
||||
endpoint,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"unknown",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
request_id,
|
||||
cf_ray,
|
||||
auth_error,
|
||||
auth_error_code,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn record_api_request_with_endpoint_details(
|
||||
&self,
|
||||
attempt: u64,
|
||||
status: Option<u16>,
|
||||
error: Option<&str>,
|
||||
duration: Duration,
|
||||
auth_header_attached: bool,
|
||||
auth_header_name: Option<&str>,
|
||||
retry_after_unauthorized: bool,
|
||||
recovery_mode: Option<&str>,
|
||||
recovery_phase: Option<&str>,
|
||||
endpoint: &str,
|
||||
residency_header_attached: bool,
|
||||
residency_header_value: Option<&str>,
|
||||
provider_header_names: Option<&str>,
|
||||
base_url_origin: &str,
|
||||
host_class: &str,
|
||||
@@ -544,8 +460,7 @@ impl SessionTelemetry {
|
||||
auth.recovery_mode = recovery_mode,
|
||||
auth.recovery_phase = recovery_phase,
|
||||
endpoint = endpoint,
|
||||
residency_header_attached = residency_header_attached,
|
||||
residency_header_value = residency_header_value,
|
||||
client_origin = client_origin_class(self.metadata.originator.as_str()),
|
||||
provider_header_names = provider_header_names,
|
||||
base_url_origin = base_url_origin,
|
||||
host_class = host_class,
|
||||
@@ -569,7 +484,6 @@ impl SessionTelemetry {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn record_websocket_connect(
|
||||
&self,
|
||||
duration: Duration,
|
||||
@@ -581,59 +495,6 @@ impl SessionTelemetry {
|
||||
recovery_mode: Option<&str>,
|
||||
recovery_phase: Option<&str>,
|
||||
endpoint: &str,
|
||||
connection_reused: bool,
|
||||
request_id: Option<&str>,
|
||||
cf_ray: Option<&str>,
|
||||
auth_error: Option<&str>,
|
||||
auth_error_code: Option<&str>,
|
||||
) {
|
||||
self.record_websocket_connect_with_endpoint_details(
|
||||
duration,
|
||||
status,
|
||||
error,
|
||||
auth_header_attached,
|
||||
auth_header_name,
|
||||
retry_after_unauthorized,
|
||||
recovery_mode,
|
||||
recovery_phase,
|
||||
endpoint,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"unknown",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
connection_reused,
|
||||
request_id,
|
||||
cf_ray,
|
||||
auth_error,
|
||||
auth_error_code,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn record_websocket_connect_with_endpoint_details(
|
||||
&self,
|
||||
duration: Duration,
|
||||
status: Option<u16>,
|
||||
error: Option<&str>,
|
||||
auth_header_attached: bool,
|
||||
auth_header_name: Option<&str>,
|
||||
retry_after_unauthorized: bool,
|
||||
recovery_mode: Option<&str>,
|
||||
recovery_phase: Option<&str>,
|
||||
endpoint: &str,
|
||||
residency_header_attached: bool,
|
||||
residency_header_value: Option<&str>,
|
||||
provider_header_names: Option<&str>,
|
||||
base_url_origin: &str,
|
||||
host_class: &str,
|
||||
@@ -672,8 +533,7 @@ impl SessionTelemetry {
|
||||
auth.recovery_mode = recovery_mode,
|
||||
auth.recovery_phase = recovery_phase,
|
||||
endpoint = endpoint,
|
||||
residency_header_attached = residency_header_attached,
|
||||
residency_header_value = residency_header_value,
|
||||
client_origin = client_origin_class(self.metadata.originator.as_str()),
|
||||
provider_header_names = provider_header_names,
|
||||
base_url_origin = base_url_origin,
|
||||
host_class = host_class,
|
||||
@@ -698,47 +558,6 @@ impl SessionTelemetry {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn record_geo_denial(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
auth_header_attached: bool,
|
||||
auth_header_name: Option<&str>,
|
||||
residency_header_attached: bool,
|
||||
residency_header_value: Option<&str>,
|
||||
provider_header_names: Option<&str>,
|
||||
http_status: Option<u16>,
|
||||
request_id: Option<&str>,
|
||||
cf_ray: Option<&str>,
|
||||
auth_error: Option<&str>,
|
||||
auth_error_code: Option<&str>,
|
||||
error_body_class: &str,
|
||||
safe_error_message: Option<&str>,
|
||||
) {
|
||||
log_and_trace_event!(
|
||||
self,
|
||||
common: {
|
||||
event.name = "codex.geo_denial",
|
||||
geo_denial_detected = true,
|
||||
request_id = request_id,
|
||||
cf_ray = cf_ray,
|
||||
endpoint = endpoint,
|
||||
auth.header_attached = auth_header_attached,
|
||||
auth.header_name = auth_header_name,
|
||||
residency_header_attached = residency_header_attached,
|
||||
residency_header_value = residency_header_value,
|
||||
provider_header_names = provider_header_names,
|
||||
http_status = http_status,
|
||||
auth.error = auth_error,
|
||||
auth.error_code = auth_error_code,
|
||||
error_body_class = error_body_class,
|
||||
safe_error_message = safe_error_message,
|
||||
},
|
||||
log: {},
|
||||
trace: {},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn record_websocket_request(
|
||||
&self,
|
||||
duration: Duration,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_otel::AuthEnvTelemetryMetadata;
|
||||
use codex_otel::OtelProvider;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
@@ -18,6 +19,9 @@ use tracing_subscriber::filter::filter_fn;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
@@ -76,6 +80,17 @@ fn find_span_event_by_name_attr<'a>(
|
||||
.unwrap_or_else(|| panic!("missing span event: {event_name}"))
|
||||
}
|
||||
|
||||
fn auth_env_metadata() -> AuthEnvTelemetryMetadata {
|
||||
AuthEnvTelemetryMetadata {
|
||||
openai_api_key_env_present: true,
|
||||
codex_api_key_env_present: false,
|
||||
codex_api_key_env_enabled: true,
|
||||
provider_env_key_name: Some("configured".to_string()),
|
||||
provider_env_key_present: Some(true),
|
||||
refresh_token_url_override_present: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otel_export_routing_policy_routes_user_prompt_log_and_trace_events() {
|
||||
let log_exporter = InMemoryLogExporter::default();
|
||||
@@ -482,10 +497,22 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
true,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
)
|
||||
.with_auth_env(auth_env_metadata());
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.record_api_request_with_endpoint_details(
|
||||
manager.conversation_starts(
|
||||
"openai",
|
||||
None,
|
||||
ReasoningSummary::Auto,
|
||||
None,
|
||||
None,
|
||||
AskForApproval::Never,
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
Vec::new(),
|
||||
None,
|
||||
);
|
||||
manager.record_api_request(
|
||||
1,
|
||||
Some(401),
|
||||
Some("http 401"),
|
||||
@@ -496,25 +523,10 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
Some("managed"),
|
||||
Some("refresh_token"),
|
||||
"/responses",
|
||||
true,
|
||||
Some("us"),
|
||||
Some("openai-project,version"),
|
||||
"chatgpt.com",
|
||||
"openai_chatgpt",
|
||||
"ide_settings",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Some("OPENAI_API_KEY"),
|
||||
Some(true),
|
||||
true,
|
||||
Some("req-401"),
|
||||
Some("ray-401"),
|
||||
Some("missing_authorization_header"),
|
||||
Some("token_expired"),
|
||||
Some("workspace_not_authorized_in_region"),
|
||||
Some("Workspace is not authorized in this region."),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -522,6 +534,20 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
tracer_provider.force_flush().expect("flush traces");
|
||||
|
||||
let logs = log_exporter.get_emitted_logs().expect("log export");
|
||||
let conversation_log = find_log_by_event_name(&logs, "codex.conversation_starts");
|
||||
let conversation_log_attrs = log_attributes(&conversation_log.record);
|
||||
assert_eq!(
|
||||
conversation_log_attrs
|
||||
.get("auth.env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_log_attrs
|
||||
.get("auth.env_provider_key_name")
|
||||
.map(String::as_str),
|
||||
Some("configured")
|
||||
);
|
||||
let request_log = find_log_by_event_name(&logs, "codex.api_request");
|
||||
let request_log_attrs = log_attributes(&request_log.record);
|
||||
assert_eq!(
|
||||
@@ -558,73 +584,33 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
request_log_attrs.get("endpoint").map(String::as_str),
|
||||
Some("/responses")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("residency_header_attached")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("residency_header_value")
|
||||
.map(String::as_str),
|
||||
Some("us")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs.get("base_url_origin").map(String::as_str),
|
||||
Some("chatgpt.com")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs.get("host_class").map(String::as_str),
|
||||
Some("openai_chatgpt")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs.get("base_url_source").map(String::as_str),
|
||||
Some("ide_settings")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("auth.env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("auth.env_provider_key_name")
|
||||
.map(String::as_str),
|
||||
Some("OPENAI_API_KEY")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("openai-project,version")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("base_url_is_default")
|
||||
.map(String::as_str),
|
||||
Some("false")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs.get("auth.error").map(String::as_str),
|
||||
Some("missing_authorization_header")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("error_body_class")
|
||||
.get("auth.env_codex_api_key_enabled")
|
||||
.map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("safe_error_message")
|
||||
.get("auth.env_refresh_token_url_override_present")
|
||||
.map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
Some("true")
|
||||
);
|
||||
assert!(!request_log_attrs.contains_key("error_body"));
|
||||
|
||||
let spans = span_exporter.get_finished_spans().expect("span export");
|
||||
let conversation_trace_event =
|
||||
find_span_event_by_name_attr(&spans[0].events.events, "codex.conversation_starts");
|
||||
let conversation_trace_attrs = span_event_attributes(conversation_trace_event);
|
||||
assert_eq!(
|
||||
conversation_trace_attrs
|
||||
.get("auth.env_provider_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
let request_trace_event =
|
||||
find_span_event_by_name_attr(&spans[0].events.events, "codex.api_request");
|
||||
let request_trace_attrs = span_event_attributes(request_trace_event);
|
||||
@@ -646,21 +632,15 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
request_trace_attrs
|
||||
.get("base_url_origin")
|
||||
.map(String::as_str),
|
||||
Some("chatgpt.com")
|
||||
);
|
||||
assert_eq!(
|
||||
request_trace_attrs.get("endpoint").map(String::as_str),
|
||||
Some("/responses")
|
||||
);
|
||||
assert_eq!(
|
||||
request_trace_attrs
|
||||
.get("safe_error_message")
|
||||
.get("auth.env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
Some("true")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -702,10 +682,11 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
true,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
)
|
||||
.with_auth_env(auth_env_metadata());
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.record_websocket_connect_with_endpoint_details(
|
||||
manager.record_websocket_connect(
|
||||
std::time::Duration::from_millis(17),
|
||||
Some(401),
|
||||
Some("http 401"),
|
||||
@@ -715,26 +696,11 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
Some("managed"),
|
||||
Some("reload"),
|
||||
"/responses",
|
||||
true,
|
||||
Some("us"),
|
||||
Some("x-api-key"),
|
||||
"openrouter.ai",
|
||||
"known_third_party",
|
||||
"config_toml",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Some("OPENROUTER_API_KEY"),
|
||||
Some(true),
|
||||
false,
|
||||
false,
|
||||
Some("req-ws-401"),
|
||||
Some("ray-ws-401"),
|
||||
Some("missing_authorization_header"),
|
||||
Some("token_expired"),
|
||||
Some("workspace_not_authorized_in_region"),
|
||||
Some("Workspace is not authorized in this region."),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -750,12 +716,6 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("auth.env_provider_key_name")
|
||||
.map(String::as_str),
|
||||
Some("OPENROUTER_API_KEY")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("auth.header_name")
|
||||
@@ -766,42 +726,22 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
connect_log_attrs.get("auth.error").map(String::as_str),
|
||||
Some("missing_authorization_header")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("x-api-key")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("base_url_origin").map(String::as_str),
|
||||
Some("openrouter.ai")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("host_class").map(String::as_str),
|
||||
Some("known_third_party")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("endpoint").map(String::as_str),
|
||||
Some("/responses")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("residency_header_value")
|
||||
.map(String::as_str),
|
||||
Some("us")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("safe_error_message")
|
||||
.map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("auth.connection_reused")
|
||||
.map(String::as_str),
|
||||
Some("false")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("auth.env_provider_key_name")
|
||||
.map(String::as_str),
|
||||
Some("configured")
|
||||
);
|
||||
|
||||
let spans = span_exporter.get_finished_spans().expect("span export");
|
||||
let connect_trace_event =
|
||||
@@ -815,21 +755,9 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
);
|
||||
assert_eq!(
|
||||
connect_trace_attrs
|
||||
.get("base_url_source")
|
||||
.get("auth.env_refresh_token_url_override_present")
|
||||
.map(String::as_str),
|
||||
Some("config_toml")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_trace_attrs
|
||||
.get("error_body_class")
|
||||
.map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_trace_attrs
|
||||
.get("safe_error_message")
|
||||
.map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
Some("true")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -871,7 +799,8 @@ fn otel_export_routing_policy_routes_websocket_request_transport_observability()
|
||||
true,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
)
|
||||
.with_auth_env(auth_env_metadata());
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.record_websocket_request(
|
||||
@@ -897,6 +826,12 @@ fn otel_export_routing_policy_routes_websocket_request_transport_observability()
|
||||
request_log_attrs.get("error.message").map(String::as_str),
|
||||
Some("stream error")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("auth.env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
|
||||
let spans = span_exporter.get_finished_spans().expect("span export");
|
||||
let request_trace_event =
|
||||
@@ -908,246 +843,10 @@ fn otel_export_routing_policy_routes_websocket_request_transport_observability()
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otel_export_routing_policy_routes_geo_denial_log_and_trace_events() {
|
||||
let log_exporter = InMemoryLogExporter::default();
|
||||
let logger_provider = SdkLoggerProvider::builder()
|
||||
.with_simple_exporter(log_exporter.clone())
|
||||
.build();
|
||||
let span_exporter = InMemorySpanExporter::default();
|
||||
let tracer_provider = SdkTracerProvider::builder()
|
||||
.with_simple_exporter(span_exporter.clone())
|
||||
.build();
|
||||
let tracer = tracer_provider.tracer("sink-split-test");
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(
|
||||
opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(
|
||||
&logger_provider,
|
||||
)
|
||||
.with_filter(filter_fn(OtelProvider::log_export_filter)),
|
||||
)
|
||||
.with(
|
||||
tracing_opentelemetry::layer()
|
||||
.with_tracer(tracer)
|
||||
.with_filter(filter_fn(OtelProvider::trace_export_filter)),
|
||||
);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::callsite::rebuild_interest_cache();
|
||||
let manager = SessionTelemetry::new(
|
||||
ThreadId::new(),
|
||||
"gpt-5.1",
|
||||
"gpt-5.1",
|
||||
Some("account-id".to_string()),
|
||||
Some("engineer@example.com".to_string()),
|
||||
Some(TelemetryAuthMode::Chatgpt),
|
||||
"codex_exec".to_string(),
|
||||
true,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.record_geo_denial(
|
||||
"/responses",
|
||||
true,
|
||||
Some("authorization"),
|
||||
true,
|
||||
Some("us"),
|
||||
Some("x-api-key"),
|
||||
Some(401),
|
||||
Some("req-geo"),
|
||||
Some("ray-geo"),
|
||||
Some("missing_authorization_header"),
|
||||
Some("workspace_not_authorized_in_region"),
|
||||
"workspace_not_authorized_in_region",
|
||||
Some("Workspace is not authorized in this region."),
|
||||
);
|
||||
});
|
||||
|
||||
logger_provider.force_flush().expect("flush logs");
|
||||
tracer_provider.force_flush().expect("flush traces");
|
||||
|
||||
let logs = log_exporter.get_emitted_logs().expect("log export");
|
||||
let geo_log = find_log_by_event_name(&logs, "codex.geo_denial");
|
||||
let geo_log_attrs = log_attributes(&geo_log.record);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("geo_denial_detected").map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("request_id").map(String::as_str),
|
||||
Some("req-geo")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs
|
||||
.get("auth.header_attached")
|
||||
request_trace_attrs
|
||||
.get("auth.env_provider_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("auth.header_name").map(String::as_str),
|
||||
Some("authorization")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("endpoint").map(String::as_str),
|
||||
Some("/responses")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("x-api-key")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("auth.error_code").map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs
|
||||
.get("residency_header_value")
|
||||
.map(String::as_str),
|
||||
Some("us")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("error_body_class").map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_log_attrs.get("safe_error_message").map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
);
|
||||
assert!(!geo_log_attrs.contains_key("error_body"));
|
||||
|
||||
let spans = span_exporter.get_finished_spans().expect("span export");
|
||||
let geo_trace_event = find_span_event_by_name_attr(&spans[0].events.events, "codex.geo_denial");
|
||||
let geo_trace_attrs = span_event_attributes(geo_trace_event);
|
||||
assert_eq!(
|
||||
geo_trace_attrs
|
||||
.get("auth.header_attached")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_trace_attrs.get("cf_ray").map(String::as_str),
|
||||
Some("ray-geo")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_trace_attrs.get("auth.error").map(String::as_str),
|
||||
Some("missing_authorization_header")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_trace_attrs.get("http_status").map(String::as_str),
|
||||
Some("401")
|
||||
);
|
||||
assert_eq!(
|
||||
geo_trace_attrs
|
||||
.get("safe_error_message")
|
||||
.map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otel_export_routing_policy_routes_conversation_start_endpoint_config() {
|
||||
let log_exporter = InMemoryLogExporter::default();
|
||||
let logger_provider = SdkLoggerProvider::builder()
|
||||
.with_simple_exporter(log_exporter.clone())
|
||||
.build();
|
||||
let span_exporter = InMemorySpanExporter::default();
|
||||
let tracer_provider = SdkTracerProvider::builder()
|
||||
.with_simple_exporter(span_exporter.clone())
|
||||
.build();
|
||||
let tracer = tracer_provider.tracer("sink-split-test");
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(
|
||||
opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(
|
||||
&logger_provider,
|
||||
)
|
||||
.with_filter(filter_fn(OtelProvider::log_export_filter)),
|
||||
)
|
||||
.with(
|
||||
tracing_opentelemetry::layer()
|
||||
.with_tracer(tracer)
|
||||
.with_filter(filter_fn(OtelProvider::trace_export_filter)),
|
||||
);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::callsite::rebuild_interest_cache();
|
||||
let manager = SessionTelemetry::new(
|
||||
ThreadId::new(),
|
||||
"gpt-5.1",
|
||||
"gpt-5.1",
|
||||
Some("account-id".to_string()),
|
||||
Some("engineer@example.com".to_string()),
|
||||
Some(TelemetryAuthMode::Chatgpt),
|
||||
"codex_exec".to_string(),
|
||||
true,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.conversation_starts_with_endpoint_details(
|
||||
"OpenAI",
|
||||
"custom",
|
||||
"custom_unknown",
|
||||
"env",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Some("OPENAI_API_KEY"),
|
||||
Some(true),
|
||||
true,
|
||||
None,
|
||||
codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
None,
|
||||
None,
|
||||
codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
Vec::new(),
|
||||
None,
|
||||
);
|
||||
});
|
||||
|
||||
logger_provider.force_flush().expect("flush logs");
|
||||
tracer_provider.force_flush().expect("flush traces");
|
||||
|
||||
let logs = log_exporter.get_emitted_logs().expect("log export");
|
||||
let start_log = find_log_by_event_name(&logs, "codex.conversation_starts");
|
||||
let start_log_attrs = log_attributes(&start_log.record);
|
||||
assert_eq!(
|
||||
start_log_attrs.get("base_url_origin").map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
assert_eq!(
|
||||
start_log_attrs.get("host_class").map(String::as_str),
|
||||
Some("custom_unknown")
|
||||
);
|
||||
assert_eq!(
|
||||
start_log_attrs.get("base_url_source").map(String::as_str),
|
||||
Some("env")
|
||||
);
|
||||
assert_eq!(
|
||||
start_log_attrs
|
||||
.get("auth.env_provider_key_name")
|
||||
.map(String::as_str),
|
||||
Some("OPENAI_API_KEY")
|
||||
);
|
||||
|
||||
let spans = span_exporter.get_finished_spans().expect("span export");
|
||||
let start_trace_event =
|
||||
find_span_event_by_name_attr(&spans[0].events.events, "codex.conversation_starts");
|
||||
let start_trace_attrs = span_event_attributes(start_trace_event);
|
||||
assert_eq!(
|
||||
start_trace_attrs
|
||||
.get("base_url_is_default")
|
||||
.map(String::as_str),
|
||||
Some("false")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user