mirror of
https://github.com/openai/codex.git
synced 2026-03-23 08:36:30 +03:00
Compare commits
1 Commits
starr/exec
...
ccy/codex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d98e49f7d |
@@ -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;
|
||||
@@ -412,6 +413,29 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
}
|
||||
|
||||
fn app_server_feedback_auth_state(
|
||||
requires_openai_auth: bool,
|
||||
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
|
||||
};
|
||||
|
||||
if has_required_auth {
|
||||
"connected"
|
||||
} else {
|
||||
"unauthed"
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(crate) fn clear_plugin_related_caches(&self) {
|
||||
self.thread_manager.plugins_manager().clear_cache();
|
||||
@@ -6897,6 +6921,17 @@ 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 {
|
||||
@@ -8267,6 +8302,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_feedback_auth_state_matches_current_auth_snapshot() {
|
||||
let no_provider_auth = ModelProviderInfo {
|
||||
name: "provider".to_string(),
|
||||
base_url: None,
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
wire_api: codex_core::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,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: true,
|
||||
supports_websockets: false,
|
||||
};
|
||||
assert_eq!(
|
||||
app_server_feedback_auth_state(true, None, &no_provider_auth),
|
||||
"unauthed"
|
||||
);
|
||||
assert_eq!(
|
||||
app_server_feedback_auth_state(false, None, &no_provider_auth),
|
||||
"connected"
|
||||
);
|
||||
|
||||
let api_key_auth = CodexAuth::from_api_key("sk-test-key");
|
||||
assert_eq!(
|
||||
app_server_feedback_auth_state(true, Some(&api_key_auth), &no_provider_auth),
|
||||
"connected"
|
||||
);
|
||||
assert_eq!(
|
||||
app_server_feedback_auth_state(false, Some(&api_key_auth), &no_provider_auth),
|
||||
"connected"
|
||||
);
|
||||
|
||||
let chatgpt_auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
assert_eq!(
|
||||
app_server_feedback_auth_state(true, Some(&chatgpt_auth), &no_provider_auth),
|
||||
"connected"
|
||||
);
|
||||
|
||||
let provider_bearer_auth = ModelProviderInfo {
|
||||
experimental_bearer_token: Some("provider-token".to_string()),
|
||||
..no_provider_auth
|
||||
};
|
||||
assert_eq!(
|
||||
app_server_feedback_auth_state(true, None, &provider_bearer_auth),
|
||||
"connected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> {
|
||||
let conversation_id = ThreadId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?;
|
||||
|
||||
@@ -96,6 +96,8 @@ use crate::client_common::ResponseEvent;
|
||||
use crate::client_common::ResponseStream;
|
||||
use crate::config::Config;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::endpoint_config_telemetry::EndpointConfigTelemetry;
|
||||
use crate::endpoint_config_telemetry::EndpointConfigTelemetrySource;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result;
|
||||
use crate::flags::CODEX_RS_SSE_FIXTURE;
|
||||
@@ -108,7 +110,7 @@ use crate::response_debug_context::telemetry_transport_error_message;
|
||||
use crate::tools::spec::create_tools_json_for_responses_api;
|
||||
use crate::util::FeedbackRequestTags;
|
||||
use crate::util::emit_feedback_auth_recovery_tags;
|
||||
use crate::util::emit_feedback_request_tags_with_auth_env;
|
||||
use crate::util::emit_feedback_request_tags;
|
||||
|
||||
pub const OPENAI_BETA_HEADER: &str = "OpenAI-Beta";
|
||||
pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
|
||||
@@ -140,6 +142,7 @@ struct ModelClientState {
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
conversation_id: ThreadId,
|
||||
provider: ModelProviderInfo,
|
||||
endpoint_telemetry_source: EndpointConfigTelemetrySource,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
session_source: SessionSource,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
@@ -159,6 +162,8 @@ struct CurrentClientSetup {
|
||||
auth: Option<CodexAuth>,
|
||||
api_provider: codex_api::Provider,
|
||||
api_auth: CoreAuthProvider,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -269,6 +274,35 @@ impl ModelClient {
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
beta_features_header: Option<String>,
|
||||
) -> Self {
|
||||
let endpoint_telemetry_source =
|
||||
EndpointConfigTelemetrySource::for_provider_without_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>>,
|
||||
conversation_id: ThreadId,
|
||||
provider: ModelProviderInfo,
|
||||
endpoint_telemetry_source: EndpointConfigTelemetrySource,
|
||||
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 codex_api_key_env_enabled = auth_manager
|
||||
.as_ref()
|
||||
@@ -279,6 +313,7 @@ impl ModelClient {
|
||||
auth_manager,
|
||||
conversation_id,
|
||||
provider,
|
||||
endpoint_telemetry_source,
|
||||
auth_env_telemetry,
|
||||
session_source,
|
||||
model_verbosity,
|
||||
@@ -369,8 +404,10 @@ impl ModelClient {
|
||||
&client_setup.api_auth,
|
||||
PendingUnauthorizedRetry::default(),
|
||||
),
|
||||
client_setup.endpoint_telemetry,
|
||||
RequestRouteTelemetry::for_endpoint(RESPONSES_COMPACT_ENDPOINT),
|
||||
self.state.auth_env_telemetry.clone(),
|
||||
client_setup.provider_header_names.clone(),
|
||||
);
|
||||
let client =
|
||||
ApiCompactClient::new(transport, client_setup.api_provider, client_setup.api_auth)
|
||||
@@ -438,8 +475,10 @@ impl ModelClient {
|
||||
&client_setup.api_auth,
|
||||
PendingUnauthorizedRetry::default(),
|
||||
),
|
||||
client_setup.endpoint_telemetry,
|
||||
RequestRouteTelemetry::for_endpoint(MEMORIES_SUMMARIZE_ENDPOINT),
|
||||
self.state.auth_env_telemetry.clone(),
|
||||
client_setup.provider_header_names.clone(),
|
||||
);
|
||||
let client =
|
||||
ApiMemoriesClient::new(transport, client_setup.api_provider, client_setup.api_auth)
|
||||
@@ -483,14 +522,18 @@ impl ModelClient {
|
||||
fn build_request_telemetry(
|
||||
session_telemetry: &SessionTelemetry,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
) -> Arc<dyn RequestTelemetry> {
|
||||
let telemetry = Arc::new(ApiTelemetry::new(
|
||||
session_telemetry.clone(),
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
endpoint_telemetry,
|
||||
auth_env_telemetry,
|
||||
provider_header_names,
|
||||
request_route_telemetry,
|
||||
));
|
||||
let request_telemetry: Arc<dyn RequestTelemetry> = telemetry;
|
||||
request_telemetry
|
||||
@@ -546,10 +589,16 @@ impl ModelClient {
|
||||
.provider
|
||||
.to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?;
|
||||
let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?;
|
||||
let endpoint_telemetry = self
|
||||
.state
|
||||
.endpoint_telemetry_source
|
||||
.classify(api_provider.base_url.as_str());
|
||||
Ok(CurrentClientSetup {
|
||||
auth,
|
||||
api_provider,
|
||||
api_auth,
|
||||
endpoint_telemetry,
|
||||
provider_header_names: self.state.provider.telemetry_header_names(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -566,14 +615,18 @@ impl ModelClient {
|
||||
turn_state: Option<Arc<OnceLock<String>>>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
provider_header_names: Option<&str>,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
) -> std::result::Result<ApiWebSocketConnection, ApiError> {
|
||||
let headers = self.build_websocket_headers(turn_state.as_ref(), turn_metadata_header);
|
||||
let websocket_telemetry = ModelClientSession::build_websocket_telemetry(
|
||||
session_telemetry,
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
endpoint_telemetry,
|
||||
self.state.auth_env_telemetry.clone(),
|
||||
provider_header_names.map(str::to_owned),
|
||||
request_route_telemetry,
|
||||
);
|
||||
let websocket_connect_timeout = self.state.provider.websocket_connect_timeout();
|
||||
let start = Instant::now();
|
||||
@@ -598,7 +651,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(
|
||||
session_telemetry.record_websocket_connect_with_endpoint_details(
|
||||
start.elapsed(),
|
||||
status,
|
||||
error_message.as_deref(),
|
||||
@@ -608,36 +661,65 @@ impl ModelClient {
|
||||
auth_context.recovery_mode,
|
||||
auth_context.recovery_phase,
|
||||
request_route_telemetry.endpoint,
|
||||
provider_header_names,
|
||||
endpoint_telemetry.base_url_origin,
|
||||
endpoint_telemetry.host_class,
|
||||
endpoint_telemetry.base_url_source,
|
||||
endpoint_telemetry.base_url_is_default,
|
||||
/*connection_reused*/ false,
|
||||
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,
|
||||
response_debug.safe_error_message,
|
||||
);
|
||||
emit_feedback_request_tags_with_auth_env(
|
||||
&FeedbackRequestTags {
|
||||
endpoint: request_route_telemetry.endpoint,
|
||||
auth_header_attached: auth_context.auth_header_attached,
|
||||
auth_header_name: auth_context.auth_header_name,
|
||||
auth_mode: auth_context.auth_mode,
|
||||
auth_retry_after_unauthorized: Some(auth_context.retry_after_unauthorized),
|
||||
auth_recovery_mode: auth_context.recovery_mode,
|
||||
auth_recovery_phase: auth_context.recovery_phase,
|
||||
auth_connection_reused: Some(false),
|
||||
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(),
|
||||
auth_recovery_followup_success: auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(result.is_ok()),
|
||||
auth_recovery_followup_status: auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
},
|
||||
&self.state.auth_env_telemetry,
|
||||
);
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
endpoint: request_route_telemetry.endpoint,
|
||||
auth_header_attached: auth_context.auth_header_attached,
|
||||
auth_header_name: auth_context.auth_header_name,
|
||||
auth_mode: auth_context.auth_mode,
|
||||
auth_env_openai_api_key_present: self
|
||||
.state
|
||||
.auth_env_telemetry
|
||||
.openai_api_key_env_present,
|
||||
auth_env_codex_api_key_present: self.state.auth_env_telemetry.codex_api_key_env_present,
|
||||
auth_env_codex_api_key_enabled: self.state.auth_env_telemetry.codex_api_key_env_enabled,
|
||||
auth_env_provider_key_name: self
|
||||
.state
|
||||
.auth_env_telemetry
|
||||
.provider_env_key_name
|
||||
.as_deref(),
|
||||
auth_env_provider_key_present: self.state.auth_env_telemetry.provider_env_key_present,
|
||||
auth_env_refresh_token_url_override_present: self
|
||||
.state
|
||||
.auth_env_telemetry
|
||||
.refresh_token_url_override_present,
|
||||
auth_retry_after_unauthorized: Some(auth_context.retry_after_unauthorized),
|
||||
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,
|
||||
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,
|
||||
auth_recovery_followup_success: auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(result.is_ok()),
|
||||
auth_recovery_followup_status: auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
@@ -887,6 +969,7 @@ impl ModelClientSession {
|
||||
&client_setup.api_auth,
|
||||
PendingUnauthorizedRetry::default(),
|
||||
);
|
||||
let endpoint_telemetry = client_setup.endpoint_telemetry;
|
||||
let connection = self
|
||||
.client
|
||||
.connect_websocket(
|
||||
@@ -896,6 +979,8 @@ impl ModelClientSession {
|
||||
Some(Arc::clone(&self.turn_state)),
|
||||
/*turn_metadata_header*/ None,
|
||||
auth_context,
|
||||
endpoint_telemetry,
|
||||
client_setup.provider_header_names.as_deref(),
|
||||
RequestRouteTelemetry::for_endpoint(RESPONSES_ENDPOINT),
|
||||
)
|
||||
.await?;
|
||||
@@ -928,6 +1013,8 @@ impl ModelClientSession {
|
||||
turn_metadata_header,
|
||||
options,
|
||||
auth_context,
|
||||
endpoint_telemetry,
|
||||
provider_header_names,
|
||||
request_route_telemetry,
|
||||
} = params;
|
||||
let needs_new = match self.websocket_session.connection.as_ref() {
|
||||
@@ -951,6 +1038,8 @@ impl ModelClientSession {
|
||||
Some(turn_state),
|
||||
turn_metadata_header,
|
||||
auth_context,
|
||||
endpoint_telemetry,
|
||||
provider_header_names,
|
||||
request_route_telemetry,
|
||||
)
|
||||
.await
|
||||
@@ -1045,8 +1134,10 @@ impl ModelClientSession {
|
||||
let (request_telemetry, sse_telemetry) = Self::build_streaming_telemetry(
|
||||
session_telemetry,
|
||||
request_auth_context,
|
||||
RequestRouteTelemetry::for_endpoint(RESPONSES_ENDPOINT),
|
||||
client_setup.endpoint_telemetry,
|
||||
self.client.state.auth_env_telemetry.clone(),
|
||||
client_setup.provider_header_names.clone(),
|
||||
RequestRouteTelemetry::for_endpoint(RESPONSES_ENDPOINT),
|
||||
);
|
||||
let compression = self.responses_request_compression(client_setup.auth.as_ref());
|
||||
let options = self.build_responses_options(turn_metadata_header, compression);
|
||||
@@ -1130,6 +1221,7 @@ impl ModelClientSession {
|
||||
pending_retry,
|
||||
);
|
||||
let compression = self.responses_request_compression(client_setup.auth.as_ref());
|
||||
let request_route_telemetry = RequestRouteTelemetry::for_endpoint(RESPONSES_ENDPOINT);
|
||||
|
||||
let options = self.build_responses_options(turn_metadata_header, compression);
|
||||
let request = self.build_responses_request(
|
||||
@@ -1156,9 +1248,9 @@ impl ModelClientSession {
|
||||
turn_metadata_header,
|
||||
options: &options,
|
||||
auth_context: request_auth_context,
|
||||
request_route_telemetry: RequestRouteTelemetry::for_endpoint(
|
||||
RESPONSES_ENDPOINT,
|
||||
),
|
||||
endpoint_telemetry: client_setup.endpoint_telemetry,
|
||||
provider_header_names: client_setup.provider_header_names.as_deref(),
|
||||
request_route_telemetry,
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -1206,14 +1298,18 @@ impl ModelClientSession {
|
||||
fn build_streaming_telemetry(
|
||||
session_telemetry: &SessionTelemetry,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
) -> (Arc<dyn RequestTelemetry>, Arc<dyn SseTelemetry>) {
|
||||
let telemetry = Arc::new(ApiTelemetry::new(
|
||||
session_telemetry.clone(),
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
endpoint_telemetry,
|
||||
auth_env_telemetry,
|
||||
provider_header_names,
|
||||
request_route_telemetry,
|
||||
));
|
||||
let request_telemetry: Arc<dyn RequestTelemetry> = telemetry.clone();
|
||||
let sse_telemetry: Arc<dyn SseTelemetry> = telemetry;
|
||||
@@ -1224,14 +1320,18 @@ impl ModelClientSession {
|
||||
fn build_websocket_telemetry(
|
||||
session_telemetry: &SessionTelemetry,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
) -> Arc<dyn WebsocketTelemetry> {
|
||||
let telemetry = Arc::new(ApiTelemetry::new(
|
||||
session_telemetry.clone(),
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
endpoint_telemetry,
|
||||
auth_env_telemetry,
|
||||
provider_header_names,
|
||||
request_route_telemetry,
|
||||
));
|
||||
let websocket_telemetry: Arc<dyn WebsocketTelemetry> = telemetry;
|
||||
websocket_telemetry
|
||||
@@ -1554,6 +1654,8 @@ struct WebsocketConnectParams<'a> {
|
||||
turn_metadata_header: Option<&'a str>,
|
||||
options: &'a ApiResponsesOptions,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
provider_header_names: Option<&'a str>,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
}
|
||||
|
||||
@@ -1683,22 +1785,28 @@ fn api_error_http_status(error: &ApiError) -> Option<u16> {
|
||||
struct ApiTelemetry {
|
||||
session_telemetry: SessionTelemetry,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
}
|
||||
|
||||
impl ApiTelemetry {
|
||||
fn new(
|
||||
session_telemetry: SessionTelemetry,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
endpoint_telemetry: EndpointConfigTelemetry,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_telemetry,
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
endpoint_telemetry,
|
||||
auth_env_telemetry,
|
||||
provider_header_names,
|
||||
request_route_telemetry,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1716,48 +1824,70 @@ impl RequestTelemetry for ApiTelemetry {
|
||||
let debug = error
|
||||
.map(extract_response_debug_context)
|
||||
.unwrap_or_default();
|
||||
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,
|
||||
debug.request_id.as_deref(),
|
||||
debug.cf_ray.as_deref(),
|
||||
debug.auth_error.as_deref(),
|
||||
debug.auth_error_code.as_deref(),
|
||||
);
|
||||
emit_feedback_request_tags_with_auth_env(
|
||||
&FeedbackRequestTags {
|
||||
endpoint: self.request_route_telemetry.endpoint,
|
||||
auth_header_attached: self.auth_context.auth_header_attached,
|
||||
auth_header_name: self.auth_context.auth_header_name,
|
||||
auth_mode: self.auth_context.auth_mode,
|
||||
auth_retry_after_unauthorized: Some(self.auth_context.retry_after_unauthorized),
|
||||
auth_recovery_mode: self.auth_context.recovery_mode,
|
||||
auth_recovery_phase: self.auth_context.recovery_phase,
|
||||
auth_connection_reused: None,
|
||||
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(),
|
||||
auth_recovery_followup_success: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(error.is_none()),
|
||||
auth_recovery_followup_status: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
},
|
||||
&self.auth_env_telemetry,
|
||||
);
|
||||
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.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,
|
||||
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,
|
||||
auth_header_name: self.auth_context.auth_header_name,
|
||||
auth_mode: self.auth_context.auth_mode,
|
||||
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,
|
||||
auth_retry_after_unauthorized: Some(self.auth_context.retry_after_unauthorized),
|
||||
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,
|
||||
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,
|
||||
auth_recovery_followup_success: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(error.is_none()),
|
||||
auth_recovery_followup_status: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1786,32 +1916,46 @@ impl WebsocketTelemetry for ApiTelemetry {
|
||||
error_message.as_deref(),
|
||||
connection_reused,
|
||||
);
|
||||
emit_feedback_request_tags_with_auth_env(
|
||||
&FeedbackRequestTags {
|
||||
endpoint: self.request_route_telemetry.endpoint,
|
||||
auth_header_attached: self.auth_context.auth_header_attached,
|
||||
auth_header_name: self.auth_context.auth_header_name,
|
||||
auth_mode: self.auth_context.auth_mode,
|
||||
auth_retry_after_unauthorized: Some(self.auth_context.retry_after_unauthorized),
|
||||
auth_recovery_mode: self.auth_context.recovery_mode,
|
||||
auth_recovery_phase: self.auth_context.recovery_phase,
|
||||
auth_connection_reused: Some(connection_reused),
|
||||
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(),
|
||||
auth_recovery_followup_success: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(error.is_none()),
|
||||
auth_recovery_followup_status: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
},
|
||||
&self.auth_env_telemetry,
|
||||
);
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
endpoint: self.request_route_telemetry.endpoint,
|
||||
auth_header_attached: self.auth_context.auth_header_attached,
|
||||
auth_header_name: self.auth_context.auth_header_name,
|
||||
auth_mode: self.auth_context.auth_mode,
|
||||
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,
|
||||
auth_retry_after_unauthorized: Some(self.auth_context.retry_after_unauthorized),
|
||||
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,
|
||||
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,
|
||||
auth_recovery_followup_success: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(error.is_none()),
|
||||
auth_recovery_followup_status: self
|
||||
.auth_context
|
||||
.retry_after_unauthorized
|
||||
.then_some(status)
|
||||
.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
fn on_ws_event(
|
||||
|
||||
@@ -169,6 +169,7 @@ use crate::config::types::McpServerConfig;
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::context_manager::TotalTokenUsageBreakdown;
|
||||
use crate::endpoint_config_telemetry::resolve_endpoint_config_telemetry_source;
|
||||
use crate::environment_context::EnvironmentContext;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
@@ -1570,6 +1571,17 @@ impl Session {
|
||||
&session_configuration.provider,
|
||||
auth_manager.codex_api_key_env_enabled(),
|
||||
);
|
||||
let endpoint_telemetry_source = resolve_endpoint_config_telemetry_source(
|
||||
&config,
|
||||
session_configuration.session_source.clone(),
|
||||
);
|
||||
let conversation_start_base_url = session_configuration
|
||||
.provider
|
||||
.to_api_provider(auth.map(CodexAuth::auth_mode))
|
||||
.map(|provider| provider.base_url)
|
||||
.unwrap_or_default();
|
||||
let endpoint_telemetry =
|
||||
endpoint_telemetry_source.classify(conversation_start_base_url.as_str());
|
||||
let mut session_telemetry = SessionTelemetry::new(
|
||||
conversation_id,
|
||||
session_model.as_str(),
|
||||
@@ -1611,8 +1623,12 @@ impl Session {
|
||||
)],
|
||||
);
|
||||
|
||||
session_telemetry.conversation_starts(
|
||||
config.model_provider.name.as_str(),
|
||||
session_telemetry.conversation_starts_with_endpoint_details(
|
||||
session_configuration.provider.name.as_str(),
|
||||
endpoint_telemetry.base_url_origin,
|
||||
endpoint_telemetry.host_class,
|
||||
endpoint_telemetry.base_url_source,
|
||||
endpoint_telemetry.base_url_is_default,
|
||||
session_configuration.collaboration_mode.reasoning_effort(),
|
||||
config
|
||||
.model_reasoning_summary
|
||||
@@ -1796,10 +1812,11 @@ impl Session {
|
||||
network_proxy,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: state_db_ctx.clone(),
|
||||
model_client: ModelClient::new(
|
||||
model_client: ModelClient::new_with_endpoint_telemetry_source(
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
conversation_id,
|
||||
session_configuration.provider.clone(),
|
||||
endpoint_telemetry_source,
|
||||
session_configuration.session_source.clone(),
|
||||
config.model_verbosity,
|
||||
ws_version_from_features(config.as_ref()),
|
||||
|
||||
@@ -120,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();
|
||||
|
||||
@@ -30,6 +30,15 @@ fn is_first_party_chat_originator_matches_known_values() {
|
||||
assert_eq!(is_first_party_chat_originator("codex_vscode"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_origin_class_matches_known_values() {
|
||||
assert_eq!(client_origin_class(DEFAULT_ORIGINATOR), "codex_cli");
|
||||
assert_eq!(client_origin_class("codex_vscode"), "first_party_ide");
|
||||
assert_eq!(client_origin_class("Codex ChatGPT"), "first_party_ide");
|
||||
assert_eq!(client_origin_class("codex_atlas"), "first_party_chat");
|
||||
assert_eq!(client_origin_class("third_party_client"), "custom");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_client_sets_default_headers() {
|
||||
skip_if_no_network!();
|
||||
|
||||
363
codex-rs/core/src/endpoint_config_telemetry.rs
Normal file
363
codex-rs/core/src/endpoint_config_telemetry.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
use crate::config::Config;
|
||||
use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use crate::model_provider_info::ModelProviderInfo;
|
||||
use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use crate::model_provider_info::OPENAI_PROVIDER_ID;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use reqwest::Url;
|
||||
|
||||
const BASE_URL_ORIGIN_CHATGPT: &str = "chatgpt.com";
|
||||
const BASE_URL_ORIGIN_OPENAI_API: &str = "api.openai.com";
|
||||
const BASE_URL_ORIGIN_OPENROUTER: &str = "openrouter.ai";
|
||||
const BASE_URL_ORIGIN_CUSTOM: &str = "custom";
|
||||
|
||||
const HOST_CLASS_OPENAI_CHATGPT: &str = "openai_chatgpt";
|
||||
const HOST_CLASS_OPENAI_API: &str = "openai_api";
|
||||
const HOST_CLASS_KNOWN_THIRD_PARTY: &str = "known_third_party";
|
||||
const HOST_CLASS_CUSTOM_UNKNOWN: &str = "custom_unknown";
|
||||
|
||||
const BASE_URL_SOURCE_DEFAULT: &str = "default";
|
||||
const BASE_URL_SOURCE_ENV: &str = "env";
|
||||
const BASE_URL_SOURCE_CONFIG_TOML: &str = "config_toml";
|
||||
const BASE_URL_SOURCE_IDE_SETTINGS: &str = "ide_settings";
|
||||
const BASE_URL_SOURCE_MANAGED_CONFIG: &str = "managed_config";
|
||||
const BASE_URL_SOURCE_SESSION_FLAGS: &str = "session_flags";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct EndpointConfigTelemetrySource {
|
||||
pub(crate) base_url_source: &'static str,
|
||||
pub(crate) base_url_is_default: bool,
|
||||
}
|
||||
|
||||
impl EndpointConfigTelemetrySource {
|
||||
pub(crate) const fn new(base_url_source: &'static str, base_url_is_default: bool) -> Self {
|
||||
Self {
|
||||
base_url_source,
|
||||
base_url_is_default,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn classify(self, base_url: &str) -> EndpointConfigTelemetry {
|
||||
let (base_url_origin, host_class) = classify_base_url(base_url);
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin,
|
||||
host_class,
|
||||
base_url_source: self.base_url_source,
|
||||
base_url_is_default: self.base_url_is_default,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_provider(
|
||||
provider_id: &str,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> EndpointConfigTelemetrySource {
|
||||
endpoint_source_from_provider_defaults(provider_id, provider)
|
||||
}
|
||||
|
||||
pub(crate) fn for_provider_without_id(provider: &ModelProviderInfo) -> Self {
|
||||
let base_url_is_default = provider.base_url.is_none();
|
||||
let base_url_source = if base_url_is_default {
|
||||
BASE_URL_SOURCE_DEFAULT
|
||||
} else {
|
||||
BASE_URL_SOURCE_CONFIG_TOML
|
||||
};
|
||||
EndpointConfigTelemetrySource::new(base_url_source, base_url_is_default)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct EndpointConfigTelemetry {
|
||||
pub(crate) base_url_origin: &'static str,
|
||||
pub(crate) host_class: &'static str,
|
||||
pub(crate) base_url_source: &'static str,
|
||||
pub(crate) base_url_is_default: bool,
|
||||
}
|
||||
|
||||
impl Default for EndpointConfigTelemetry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url_origin: BASE_URL_ORIGIN_CUSTOM,
|
||||
host_class: HOST_CLASS_CUSTOM_UNKNOWN,
|
||||
base_url_source: BASE_URL_SOURCE_DEFAULT,
|
||||
base_url_is_default: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_endpoint_config_telemetry_source(
|
||||
config: &Config,
|
||||
session_source: SessionSource,
|
||||
) -> EndpointConfigTelemetrySource {
|
||||
resolve_endpoint_config_telemetry_source_for_provider(
|
||||
config,
|
||||
config.model_provider_id.as_str(),
|
||||
&config.model_provider,
|
||||
session_source,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_endpoint_config_telemetry_source_for_provider(
|
||||
config: &Config,
|
||||
provider_id: &str,
|
||||
provider: &ModelProviderInfo,
|
||||
session_source: SessionSource,
|
||||
) -> EndpointConfigTelemetrySource {
|
||||
let origins = config.config_layer_stack.origins();
|
||||
if provider_id == OPENAI_PROVIDER_ID
|
||||
&& let Some(origin) = origins.get("openai_base_url")
|
||||
{
|
||||
return endpoint_source_from_layer(&origin.name, session_source);
|
||||
}
|
||||
|
||||
let key = format!("model_providers.{provider_id}.base_url");
|
||||
if let Some(origin) = origins.get(&key) {
|
||||
return endpoint_source_from_layer(&origin.name, session_source);
|
||||
}
|
||||
|
||||
endpoint_source_from_provider_defaults(provider_id, provider)
|
||||
}
|
||||
|
||||
fn endpoint_source_from_layer(
|
||||
layer: &ConfigLayerSource,
|
||||
session_source: SessionSource,
|
||||
) -> EndpointConfigTelemetrySource {
|
||||
let base_url_source = match layer {
|
||||
ConfigLayerSource::SessionFlags => match session_source {
|
||||
SessionSource::VSCode | SessionSource::Mcp => BASE_URL_SOURCE_IDE_SETTINGS,
|
||||
SessionSource::Cli
|
||||
| SessionSource::Exec
|
||||
| SessionSource::SubAgent(_)
|
||||
| SessionSource::Unknown => BASE_URL_SOURCE_SESSION_FLAGS,
|
||||
},
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::Project { .. } => {
|
||||
BASE_URL_SOURCE_CONFIG_TOML
|
||||
}
|
||||
ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => BASE_URL_SOURCE_MANAGED_CONFIG,
|
||||
};
|
||||
|
||||
EndpointConfigTelemetrySource::new(base_url_source, false)
|
||||
}
|
||||
|
||||
fn endpoint_source_from_provider_defaults(
|
||||
provider_id: &str,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> EndpointConfigTelemetrySource {
|
||||
let env_source = match provider_id {
|
||||
OPENAI_PROVIDER_ID => env_var_present("OPENAI_BASE_URL"),
|
||||
OLLAMA_OSS_PROVIDER_ID | LMSTUDIO_OSS_PROVIDER_ID => {
|
||||
env_var_present("CODEX_OSS_BASE_URL") || env_var_present("CODEX_OSS_PORT")
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if env_source {
|
||||
return EndpointConfigTelemetrySource::new(BASE_URL_SOURCE_ENV, false);
|
||||
}
|
||||
|
||||
let base_url_is_default = match provider_id {
|
||||
OPENAI_PROVIDER_ID => provider.base_url.is_none(),
|
||||
OLLAMA_OSS_PROVIDER_ID | LMSTUDIO_OSS_PROVIDER_ID => true,
|
||||
_ => provider.base_url.is_none(),
|
||||
};
|
||||
if base_url_is_default {
|
||||
return EndpointConfigTelemetrySource::new(BASE_URL_SOURCE_DEFAULT, true);
|
||||
}
|
||||
|
||||
EndpointConfigTelemetrySource::new(BASE_URL_SOURCE_CONFIG_TOML, false)
|
||||
}
|
||||
|
||||
fn env_var_present(name: &str) -> bool {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn classify_base_url(base_url: &str) -> (&'static str, &'static str) {
|
||||
let Ok(url) = Url::parse(base_url) else {
|
||||
return (BASE_URL_ORIGIN_CUSTOM, HOST_CLASS_CUSTOM_UNKNOWN);
|
||||
};
|
||||
let Some(host) = url.host_str().map(str::to_ascii_lowercase) else {
|
||||
return (BASE_URL_ORIGIN_CUSTOM, HOST_CLASS_CUSTOM_UNKNOWN);
|
||||
};
|
||||
|
||||
if matches!(host.as_str(), "chatgpt.com" | "chat.openai.com") {
|
||||
if is_chatgpt_codex_path(url.path()) {
|
||||
return (BASE_URL_ORIGIN_CHATGPT, HOST_CLASS_OPENAI_CHATGPT);
|
||||
}
|
||||
return (BASE_URL_ORIGIN_CHATGPT, HOST_CLASS_CUSTOM_UNKNOWN);
|
||||
}
|
||||
|
||||
if host == BASE_URL_ORIGIN_OPENAI_API {
|
||||
return (BASE_URL_ORIGIN_OPENAI_API, HOST_CLASS_OPENAI_API);
|
||||
}
|
||||
|
||||
if host == BASE_URL_ORIGIN_OPENROUTER || host.ends_with(".openrouter.ai") {
|
||||
return (BASE_URL_ORIGIN_OPENROUTER, HOST_CLASS_KNOWN_THIRD_PARTY);
|
||||
}
|
||||
|
||||
(BASE_URL_ORIGIN_CUSTOM, HOST_CLASS_CUSTOM_UNKNOWN)
|
||||
}
|
||||
|
||||
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::WireApi;
|
||||
use crate::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,
|
||||
websocket_connect_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://api.openrouter.ai/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("http://localhost:11434/v1"),
|
||||
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/not-codex"),
|
||||
EndpointConfigTelemetry {
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "custom_unknown",
|
||||
base_url_source: "config_toml",
|
||||
base_url_is_default: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_source_maps_session_flags_by_session_source() {
|
||||
let origin = ConfigLayerSource::SessionFlags;
|
||||
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&origin, SessionSource::Cli),
|
||||
EndpointConfigTelemetrySource::new("session_flags", false)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&origin, SessionSource::Exec),
|
||||
EndpointConfigTelemetrySource::new("session_flags", false)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(
|
||||
&origin,
|
||||
SessionSource::SubAgent(codex_protocol::protocol::SubAgentSource::Review)
|
||||
),
|
||||
EndpointConfigTelemetrySource::new("session_flags", false)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&origin, SessionSource::VSCode),
|
||||
EndpointConfigTelemetrySource::new("ide_settings", false)
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&origin, SessionSource::Mcp),
|
||||
EndpointConfigTelemetrySource::new("ide_settings", false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_source_maps_managed_and_toml_layers() {
|
||||
let managed_file = ConfigLayerSource::LegacyManagedConfigTomlFromFile {
|
||||
file: AbsolutePathBuf::try_from("/tmp/managed.toml")
|
||||
.expect("managed path should be absolute"),
|
||||
};
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&managed_file, SessionSource::Cli),
|
||||
EndpointConfigTelemetrySource::new("managed_config", false)
|
||||
);
|
||||
|
||||
let user_file = ConfigLayerSource::User {
|
||||
file: AbsolutePathBuf::try_from("/tmp/config.toml")
|
||||
.expect("config path should be absolute"),
|
||||
};
|
||||
assert_eq!(
|
||||
endpoint_source_from_layer(&user_file, SessionSource::Cli),
|
||||
EndpointConfigTelemetrySource::new("config_toml", false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_source_uses_provider_defaults_for_custom_and_oss_providers() {
|
||||
let custom = provider(Some("https://example.com/v1"));
|
||||
assert_eq!(
|
||||
endpoint_source_from_provider_defaults("example", &custom),
|
||||
EndpointConfigTelemetrySource::new("config_toml", false)
|
||||
);
|
||||
|
||||
let custom_default = provider(None);
|
||||
assert_eq!(
|
||||
endpoint_source_from_provider_defaults("example", &custom_default),
|
||||
EndpointConfigTelemetrySource::new("default", true)
|
||||
);
|
||||
|
||||
let oss =
|
||||
create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses);
|
||||
assert_eq!(
|
||||
endpoint_source_from_provider_defaults(crate::OLLAMA_OSS_PROVIDER_ID, &oss),
|
||||
EndpointConfigTelemetrySource::new("default", true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ pub mod connectors;
|
||||
mod context_manager;
|
||||
mod contextual_user_message;
|
||||
pub mod custom_prompts;
|
||||
mod endpoint_config_telemetry;
|
||||
pub mod env;
|
||||
mod environment_context;
|
||||
pub mod error;
|
||||
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -130,6 +131,38 @@ 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() {
|
||||
None
|
||||
} else {
|
||||
Some(names.into_iter().collect::<Vec<_>>().join(","))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_header_map(&self) -> crate::error::Result<HeaderMap> {
|
||||
let capacity = self.http_headers.as_ref().map_or(0, HashMap::len)
|
||||
+ self.env_http_headers.as_ref().map_or(0, HashMap::len);
|
||||
|
||||
@@ -27,6 +27,10 @@ base_url = "http://localhost:11434/v1"
|
||||
|
||||
let provider: ModelProviderInfo = toml::from_str(azure_provider_toml).unwrap();
|
||||
assert_eq!(expected_provider, provider);
|
||||
assert_eq!(
|
||||
provider.telemetry_header_names().as_deref(),
|
||||
Some("x-example-header")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,6 +8,7 @@ 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::endpoint_config_telemetry::EndpointConfigTelemetrySource;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CoreResult;
|
||||
use crate::model_provider_info::ModelProviderInfo;
|
||||
@@ -17,7 +18,7 @@ use crate::models_manager::model_info;
|
||||
use crate::response_debug_context::extract_response_debug_context;
|
||||
use crate::response_debug_context::telemetry_transport_error_message;
|
||||
use crate::util::FeedbackRequestTags;
|
||||
use crate::util::emit_feedback_request_tags_with_auth_env;
|
||||
use crate::util::emit_feedback_request_tags;
|
||||
use codex_api::ModelsClient;
|
||||
use codex_api::RequestTelemetry;
|
||||
use codex_api::ReqwestTransport;
|
||||
@@ -49,6 +50,11 @@ struct ModelsRequestTelemetry {
|
||||
auth_header_attached: bool,
|
||||
auth_header_name: Option<&'static str>,
|
||||
auth_env: AuthEnvTelemetry,
|
||||
provider_header_names: Option<String>,
|
||||
base_url_origin: &'static str,
|
||||
host_class: &'static str,
|
||||
base_url_source: &'static str,
|
||||
base_url_is_default: bool,
|
||||
}
|
||||
|
||||
impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
@@ -77,6 +83,11 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
endpoint = MODELS_ENDPOINT,
|
||||
auth.header_attached = self.auth_header_attached,
|
||||
auth.header_name = self.auth_header_name,
|
||||
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,
|
||||
auth.env_openai_api_key_present = self.auth_env.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.auth_env.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.auth_env.codex_api_key_env_enabled,
|
||||
@@ -87,6 +98,8 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
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,
|
||||
auth.mode = self.auth_mode.as_deref(),
|
||||
);
|
||||
tracing::event!(
|
||||
@@ -101,6 +114,11 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
endpoint = MODELS_ENDPOINT,
|
||||
auth.header_attached = self.auth_header_attached,
|
||||
auth.header_name = self.auth_header_name,
|
||||
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,
|
||||
auth.env_openai_api_key_present = self.auth_env.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.auth_env.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.auth_env.codex_api_key_env_enabled,
|
||||
@@ -111,27 +129,43 @@ impl RequestTelemetry for ModelsRequestTelemetry {
|
||||
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,
|
||||
auth.mode = self.auth_mode.as_deref(),
|
||||
);
|
||||
emit_feedback_request_tags_with_auth_env(
|
||||
&FeedbackRequestTags {
|
||||
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_retry_after_unauthorized: None,
|
||||
auth_recovery_mode: None,
|
||||
auth_recovery_phase: None,
|
||||
auth_connection_reused: None,
|
||||
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(),
|
||||
auth_recovery_followup_success: None,
|
||||
auth_recovery_followup_status: None,
|
||||
},
|
||||
&self.auth_env,
|
||||
);
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
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.openai_api_key_env_present,
|
||||
auth_env_codex_api_key_present: self.auth_env.codex_api_key_env_present,
|
||||
auth_env_codex_api_key_enabled: self.auth_env.codex_api_key_env_enabled,
|
||||
auth_env_provider_key_name: self.auth_env.provider_env_key_name.as_deref(),
|
||||
auth_env_provider_key_present: self.auth_env.provider_env_key_present,
|
||||
auth_env_refresh_token_url_override_present: self
|
||||
.auth_env
|
||||
.refresh_token_url_override_present,
|
||||
auth_retry_after_unauthorized: None,
|
||||
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,
|
||||
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,
|
||||
auth_recovery_followup_success: None,
|
||||
auth_recovery_followup_status: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +215,7 @@ pub struct ModelsManager {
|
||||
etag: RwLock<Option<String>>,
|
||||
cache_manager: ModelsCacheManager,
|
||||
provider: ModelProviderInfo,
|
||||
endpoint_telemetry_source: EndpointConfigTelemetrySource,
|
||||
}
|
||||
|
||||
impl ModelsManager {
|
||||
@@ -211,6 +246,29 @@ impl ModelsManager {
|
||||
model_catalog: Option<ModelsResponse>,
|
||||
collaboration_modes_config: CollaborationModesConfig,
|
||||
provider: ModelProviderInfo,
|
||||
) -> Self {
|
||||
let endpoint_telemetry_source = if provider.is_openai() {
|
||||
EndpointConfigTelemetrySource::for_provider(crate::OPENAI_PROVIDER_ID, &provider)
|
||||
} else {
|
||||
EndpointConfigTelemetrySource::for_provider_without_id(&provider)
|
||||
};
|
||||
Self::new_with_provider_and_endpoint_telemetry_source(
|
||||
codex_home,
|
||||
auth_manager,
|
||||
model_catalog,
|
||||
collaboration_modes_config,
|
||||
provider,
|
||||
endpoint_telemetry_source,
|
||||
)
|
||||
}
|
||||
|
||||
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 cache_path = codex_home.join(MODEL_CACHE_FILE);
|
||||
let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL);
|
||||
@@ -233,6 +291,7 @@ impl ModelsManager {
|
||||
etag: RwLock::new(None),
|
||||
cache_manager,
|
||||
provider,
|
||||
endpoint_telemetry_source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,12 +498,20 @@ impl ModelsManager {
|
||||
&self.provider,
|
||||
self.auth_manager.codex_api_key_env_enabled(),
|
||||
);
|
||||
let endpoint_telemetry = self
|
||||
.endpoint_telemetry_source
|
||||
.classify(api_provider.base_url.as_str());
|
||||
let transport = ReqwestTransport::new(build_reqwest_client());
|
||||
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,
|
||||
provider_header_names: self.provider.telemetry_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,
|
||||
});
|
||||
let client = ModelsClient::new(transport, api_provider, api_auth)
|
||||
.with_telemetry(Some(request_telemetry));
|
||||
@@ -543,12 +610,18 @@ impl ModelsManager {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
provider: ModelProviderInfo,
|
||||
) -> Self {
|
||||
Self::new_with_provider(
|
||||
let endpoint_telemetry_source = if provider.is_openai() {
|
||||
EndpointConfigTelemetrySource::for_provider(crate::OPENAI_PROVIDER_ID, &provider)
|
||||
} else {
|
||||
EndpointConfigTelemetrySource::for_provider_without_id(&provider)
|
||||
};
|
||||
Self::new_with_provider_and_endpoint_telemetry_source(
|
||||
codex_home,
|
||||
auth_manager,
|
||||
/*model_catalog*/ None,
|
||||
CollaborationModesConfig::default(),
|
||||
provider,
|
||||
endpoint_telemetry_source,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -7,6 +7,11 @@ const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id";
|
||||
const CF_RAY_HEADER: &str = "cf-ray";
|
||||
const AUTH_ERROR_HEADER: &str = "x-openai-authorization-error";
|
||||
const X_ERROR_JSON_HEADER: &str = "x-error-json";
|
||||
const WORKSPACE_NOT_AUTHORIZED_IN_REGION_MESSAGE: &str =
|
||||
"Workspace is not authorized in this region.";
|
||||
pub(crate) const WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS: &str =
|
||||
"workspace_not_authorized_in_region";
|
||||
const MAX_ERROR_BODY_BYTES: usize = 1000;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ResponseDebugContext {
|
||||
@@ -14,15 +19,14 @@ pub(crate) struct ResponseDebugContext {
|
||||
pub(crate) cf_ray: Option<String>,
|
||||
pub(crate) auth_error: Option<String>,
|
||||
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) fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext {
|
||||
let mut context = ResponseDebugContext::default();
|
||||
|
||||
let TransportError::Http {
|
||||
headers, body: _, ..
|
||||
} = transport
|
||||
else {
|
||||
let TransportError::Http { headers, body, .. } = transport else {
|
||||
return context;
|
||||
};
|
||||
|
||||
@@ -49,6 +53,11 @@ pub(crate) fn extract_response_debug_context(transport: &TransportError) -> Resp
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string)
|
||||
});
|
||||
let error_body = extract_error_body(body.as_deref());
|
||||
context.safe_error_message = error_body
|
||||
.as_deref()
|
||||
.and_then(allowlisted_error_body_message);
|
||||
context.error_body_class = error_body.as_deref().and_then(classify_error_body_message);
|
||||
|
||||
context
|
||||
}
|
||||
@@ -87,9 +96,75 @@ pub(crate) fn telemetry_api_error_message(error: &ApiError) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_error_body(body: Option<&str>) -> Option<String> {
|
||||
let body = body?;
|
||||
if let Some(message) = extract_error_message(body) {
|
||||
return Some(message);
|
||||
}
|
||||
|
||||
let trimmed = body.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(truncate_with_ellipsis(trimmed, MAX_ERROR_BODY_BYTES))
|
||||
}
|
||||
|
||||
fn extract_error_message(body: &str) -> Option<String> {
|
||||
let json = serde_json::from_str::<serde_json::Value>(body).ok()?;
|
||||
let message = json
|
||||
.get("error")
|
||||
.and_then(|error| error.get("message"))
|
||||
.and_then(serde_json::Value::as_str)?;
|
||||
let message = message.trim();
|
||||
if message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_error_body_message(message: &str) -> Option<&'static str> {
|
||||
if message == WORKSPACE_NOT_AUTHORIZED_IN_REGION_MESSAGE {
|
||||
Some(WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn allowlisted_error_body_message(message: &str) -> Option<&'static str> {
|
||||
if message == WORKSPACE_NOT_AUTHORIZED_IN_REGION_MESSAGE {
|
||||
Some(WORKSPACE_NOT_AUTHORIZED_IN_REGION_MESSAGE)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_with_ellipsis(input: &str, max_bytes: usize) -> String {
|
||||
if input.len() <= max_bytes {
|
||||
return input.to_string();
|
||||
}
|
||||
|
||||
let ellipsis = "...";
|
||||
let keep = max_bytes.saturating_sub(ellipsis.len());
|
||||
let mut truncated = String::new();
|
||||
let mut used = 0usize;
|
||||
for ch in input.chars() {
|
||||
let len = ch.len_utf8();
|
||||
if used + len > keep {
|
||||
break;
|
||||
}
|
||||
truncated.push(ch);
|
||||
used += len;
|
||||
}
|
||||
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;
|
||||
@@ -128,6 +203,33 @@ mod tests {
|
||||
cf_ray: Some("ray-auth".to_string()),
|
||||
auth_error: Some("missing_authorization_header".to_string()),
|
||||
auth_error_code: Some("token_expired".to_string()),
|
||||
safe_error_message: None,
|
||||
error_body_class: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_response_debug_context_captures_allowlisted_safe_error_message() {
|
||||
let context = extract_response_debug_context(&TransportError::Http {
|
||||
status: StatusCode::UNAUTHORIZED,
|
||||
url: Some("https://chatgpt.com/backend-api/codex/responses".to_string()),
|
||||
headers: None,
|
||||
body: Some(
|
||||
r#"{"error":{"message":"Workspace is not authorized in this region."},"status":401}"#
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
context,
|
||||
ResponseDebugContext {
|
||||
request_id: None,
|
||||
cf_ray: None,
|
||||
auth_error: None,
|
||||
auth_error_code: None,
|
||||
safe_error_message: Some("Workspace is not authorized in this region."),
|
||||
error_body_class: Some(WORKSPACE_NOT_AUTHORIZED_IN_REGION_CLASS),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use rand::Rng;
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
|
||||
use crate::auth_env_telemetry::AuthEnvTelemetry;
|
||||
use crate::default_client::client_origin_class;
|
||||
use crate::default_client::originator;
|
||||
use crate::parse_command::shlex_join;
|
||||
|
||||
const INITIAL_DELAY_MS: u64 = 200;
|
||||
@@ -43,35 +44,33 @@ pub(crate) struct FeedbackRequestTags<'a> {
|
||||
pub auth_header_attached: bool,
|
||||
pub auth_header_name: Option<&'a str>,
|
||||
pub auth_mode: Option<&'a str>,
|
||||
pub auth_env_openai_api_key_present: bool,
|
||||
pub auth_env_codex_api_key_present: bool,
|
||||
pub auth_env_codex_api_key_enabled: bool,
|
||||
pub auth_env_provider_key_name: Option<&'a str>,
|
||||
pub auth_env_provider_key_present: Option<bool>,
|
||||
pub auth_env_refresh_token_url_override_present: bool,
|
||||
pub auth_retry_after_unauthorized: Option<bool>,
|
||||
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 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 auth_recovery_followup_success: Option<bool>,
|
||||
pub auth_recovery_followup_status: Option<u16>,
|
||||
}
|
||||
|
||||
struct FeedbackRequestSnapshot<'a> {
|
||||
endpoint: &'a str,
|
||||
auth_header_attached: bool,
|
||||
auth_header_name: &'a str,
|
||||
auth_mode: &'a str,
|
||||
auth_retry_after_unauthorized: String,
|
||||
auth_recovery_mode: &'a str,
|
||||
auth_recovery_phase: &'a str,
|
||||
auth_connection_reused: String,
|
||||
auth_request_id: &'a str,
|
||||
auth_cf_ray: &'a str,
|
||||
auth_error: &'a str,
|
||||
auth_error_code: &'a str,
|
||||
auth_recovery_followup_success: String,
|
||||
auth_recovery_followup_status: String,
|
||||
}
|
||||
|
||||
struct Auth401FeedbackSnapshot<'a> {
|
||||
request_id: &'a str,
|
||||
cf_ray: &'a str,
|
||||
@@ -95,84 +94,71 @@ impl<'a> Auth401FeedbackSnapshot<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FeedbackRequestSnapshot<'a> {
|
||||
fn from_tags(tags: &'a FeedbackRequestTags<'a>) -> Self {
|
||||
Self {
|
||||
endpoint: tags.endpoint,
|
||||
auth_header_attached: tags.auth_header_attached,
|
||||
auth_header_name: tags.auth_header_name.unwrap_or(""),
|
||||
auth_mode: tags.auth_mode.unwrap_or(""),
|
||||
auth_retry_after_unauthorized: tags
|
||||
.auth_retry_after_unauthorized
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
auth_recovery_mode: tags.auth_recovery_mode.unwrap_or(""),
|
||||
auth_recovery_phase: tags.auth_recovery_phase.unwrap_or(""),
|
||||
auth_connection_reused: tags
|
||||
.auth_connection_reused
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
auth_request_id: tags.auth_request_id.unwrap_or(""),
|
||||
auth_cf_ray: tags.auth_cf_ray.unwrap_or(""),
|
||||
auth_error: tags.auth_error.unwrap_or(""),
|
||||
auth_error_code: tags.auth_error_code.unwrap_or(""),
|
||||
auth_recovery_followup_success: tags
|
||||
.auth_recovery_followup_success
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
auth_recovery_followup_status: tags
|
||||
.auth_recovery_followup_status
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn emit_feedback_request_tags(tags: &FeedbackRequestTags<'_>) {
|
||||
let snapshot = FeedbackRequestSnapshot::from_tags(tags);
|
||||
let auth_header_name = tags.auth_header_name.unwrap_or("");
|
||||
let auth_mode = tags.auth_mode.unwrap_or("");
|
||||
let auth_env_provider_key_name = tags.auth_env_provider_key_name.unwrap_or("");
|
||||
let auth_env_provider_key_present = tags
|
||||
.auth_env_provider_key_present
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let auth_retry_after_unauthorized = tags
|
||||
.auth_retry_after_unauthorized
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
let auth_recovery_mode = tags.auth_recovery_mode.unwrap_or("");
|
||||
let auth_recovery_phase = tags.auth_recovery_phase.unwrap_or("");
|
||||
let auth_connection_reused = tags
|
||||
.auth_connection_reused
|
||||
.map_or_else(String::new, |value| value.to_string());
|
||||
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 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 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 = snapshot.endpoint,
|
||||
auth_header_attached = snapshot.auth_header_attached,
|
||||
auth_header_name = snapshot.auth_header_name,
|
||||
auth_mode = snapshot.auth_mode,
|
||||
auth_retry_after_unauthorized = snapshot.auth_retry_after_unauthorized,
|
||||
auth_recovery_mode = snapshot.auth_recovery_mode,
|
||||
auth_recovery_phase = snapshot.auth_recovery_phase,
|
||||
auth_connection_reused = snapshot.auth_connection_reused,
|
||||
auth_request_id = snapshot.auth_request_id,
|
||||
auth_cf_ray = snapshot.auth_cf_ray,
|
||||
auth_error = snapshot.auth_error,
|
||||
auth_error_code = snapshot.auth_error_code,
|
||||
auth_recovery_followup_success = snapshot.auth_recovery_followup_success,
|
||||
auth_recovery_followup_status = snapshot.auth_recovery_followup_status
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn emit_feedback_request_tags_with_auth_env(
|
||||
tags: &FeedbackRequestTags<'_>,
|
||||
auth_env: &AuthEnvTelemetry,
|
||||
) {
|
||||
let snapshot = FeedbackRequestSnapshot::from_tags(tags);
|
||||
feedback_tags!(
|
||||
endpoint = snapshot.endpoint,
|
||||
auth_header_attached = snapshot.auth_header_attached,
|
||||
auth_header_name = snapshot.auth_header_name,
|
||||
auth_mode = snapshot.auth_mode,
|
||||
auth_retry_after_unauthorized = snapshot.auth_retry_after_unauthorized,
|
||||
auth_recovery_mode = snapshot.auth_recovery_mode,
|
||||
auth_recovery_phase = snapshot.auth_recovery_phase,
|
||||
auth_connection_reused = snapshot.auth_connection_reused,
|
||||
auth_request_id = snapshot.auth_request_id,
|
||||
auth_cf_ray = snapshot.auth_cf_ray,
|
||||
auth_error = snapshot.auth_error,
|
||||
auth_error_code = snapshot.auth_error_code,
|
||||
auth_recovery_followup_success = snapshot.auth_recovery_followup_success,
|
||||
auth_recovery_followup_status = snapshot.auth_recovery_followup_status,
|
||||
auth_env_openai_api_key_present = auth_env.openai_api_key_env_present,
|
||||
auth_env_codex_api_key_present = auth_env.codex_api_key_env_present,
|
||||
auth_env_codex_api_key_enabled = auth_env.codex_api_key_env_enabled,
|
||||
auth_env_provider_key_name = auth_env.provider_env_key_name.as_deref().unwrap_or(""),
|
||||
auth_env_provider_key_present = auth_env
|
||||
.provider_env_key_present
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
auth_env_refresh_token_url_override_present = auth_env.refresh_token_url_override_present
|
||||
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,
|
||||
auth_env_openai_api_key_present = tags.auth_env_openai_api_key_present,
|
||||
auth_env_codex_api_key_present = tags.auth_env_codex_api_key_present,
|
||||
auth_env_codex_api_key_enabled = tags.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 =
|
||||
tags.auth_env_refresh_token_url_override_present,
|
||||
auth_retry_after_unauthorized = auth_retry_after_unauthorized,
|
||||
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,
|
||||
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,
|
||||
auth_recovery_followup_success = auth_recovery_followup_success,
|
||||
auth_recovery_followup_status = auth_recovery_followup_status
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use crate::auth_env_telemetry::AuthEnvTelemetry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -69,7 +68,6 @@ 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
|
||||
@@ -83,55 +81,57 @@ 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(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.set_default();
|
||||
|
||||
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,
|
||||
);
|
||||
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),
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names: Some("openai-project"),
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: 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"),
|
||||
error_body_class: Some("workspace_not_authorized_in_region"),
|
||||
safe_error_message: Some("Workspace is not authorized in this region."),
|
||||
auth_recovery_followup_success: Some(true),
|
||||
auth_recovery_followup_status: Some(200),
|
||||
});
|
||||
|
||||
let tags = tags.lock().unwrap().clone();
|
||||
assert_eq!(
|
||||
tags.get("endpoint").map(String::as_str),
|
||||
Some("\"/responses\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("client_origin").map(String::as_str),
|
||||
Some("\"codex_cli\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_header_attached").map(String::as_str),
|
||||
Some("true")
|
||||
@@ -141,38 +141,18 @@ fn emit_feedback_request_tags_records_sentry_feedback_fields() {
|
||||
Some("\"authorization\"")
|
||||
);
|
||||
assert_eq!(
|
||||
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")
|
||||
tags.get("auth_request_id").map(String::as_str),
|
||||
Some("\"req-123\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_provider_key_name").map(String::as_str),
|
||||
Some("\"configured\"")
|
||||
Some("\"OPENAI_API_KEY\"")
|
||||
);
|
||||
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_request_id").map(String::as_str),
|
||||
Some("\"req-123\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_error_code").map(String::as_str),
|
||||
Some("\"token_expired\"")
|
||||
@@ -187,18 +167,13 @@ 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(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.set_default();
|
||||
|
||||
emit_feedback_auth_recovery_tags(
|
||||
@@ -228,18 +203,13 @@ 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(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.set_default();
|
||||
|
||||
emit_feedback_auth_recovery_tags(
|
||||
@@ -275,18 +245,13 @@ 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_preserves_latest_auth_fields_after_unauthorized() {
|
||||
fn emit_feedback_request_tags_clears_stale_latest_auth_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(),
|
||||
event_count: event_count.clone(),
|
||||
})
|
||||
.with(TagCollectorLayer { tags: tags.clone() })
|
||||
.set_default();
|
||||
|
||||
emit_feedback_request_tags(&FeedbackRequestTags {
|
||||
@@ -294,95 +259,60 @@ fn emit_feedback_request_tags_preserves_latest_auth_fields_after_unauthorized()
|
||||
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: None,
|
||||
auth_connection_reused: Some(true),
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names: Some("openai-project"),
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: 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(false),
|
||||
auth_recovery_followup_status: Some(401),
|
||||
error_body_class: Some("workspace_not_authorized_in_region"),
|
||||
safe_error_message: Some("Workspace is not authorized in this region."),
|
||||
auth_recovery_followup_success: Some(true),
|
||||
auth_recovery_followup_status: Some(200),
|
||||
});
|
||||
|
||||
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,
|
||||
app_server_auth_state: None,
|
||||
app_server_requires_openai_auth: None,
|
||||
provider_header_names: None,
|
||||
base_url_origin: "chatgpt.com",
|
||||
host_class: "openai_chatgpt",
|
||||
base_url_source: "default",
|
||||
base_url_is_default: true,
|
||||
auth_request_id: None,
|
||||
auth_cf_ray: None,
|
||||
auth_error: None,
|
||||
auth_error_code: None,
|
||||
error_body_class: None,
|
||||
safe_error_message: None,
|
||||
auth_recovery_followup_success: None,
|
||||
auth_recovery_followup_status: None,
|
||||
});
|
||||
@@ -393,6 +323,28 @@ fn emit_feedback_request_tags_preserves_auth_env_fields_for_legacy_emitters() {
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(tags.get("auth_mode").map(String::as_str), Some("\"\""));
|
||||
assert_eq!(
|
||||
tags.get("app_server_auth_state").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("app_server_requires_openai_auth")
|
||||
.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("auth_request_id").map(String::as_str),
|
||||
Some("\"\"")
|
||||
@@ -404,33 +356,12 @@ fn emit_feedback_request_tags_preserves_auth_env_fields_for_legacy_emitters() {
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_openai_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
tags.get("error_body_class").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_env_codex_api_key_present")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
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("\"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")
|
||||
tags.get("safe_error_message").map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(
|
||||
tags.get("auth_recovery_followup_success")
|
||||
@@ -442,7 +373,6 @@ fn emit_feedback_request_tags_preserves_auth_env_fields_for_legacy_emitters() {
|
||||
.map(String::as_str),
|
||||
Some("\"\"")
|
||||
);
|
||||
assert_eq!(*event_count.lock().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -72,6 +72,18 @@ pub struct AuthEnvTelemetryMetadata {
|
||||
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,
|
||||
@@ -320,12 +332,51 @@ 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,
|
||||
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,
|
||||
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,
|
||||
client_origin = client_origin_class(self.metadata.originator.as_str()),
|
||||
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 = self.metadata.auth_env.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.metadata.auth_env.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.metadata.auth_env.codex_api_key_env_enabled,
|
||||
@@ -363,21 +414,28 @@ 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(
|
||||
self.record_api_request_with_endpoint_details(
|
||||
attempt,
|
||||
status,
|
||||
error.as_deref(),
|
||||
duration,
|
||||
/*auth_header_attached*/ false,
|
||||
/*auth_header_name*/ None,
|
||||
/*retry_after_unauthorized*/ false,
|
||||
/*recovery_mode*/ None,
|
||||
/*recovery_phase*/ None,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
"unknown",
|
||||
/*request_id*/ None,
|
||||
/*cf_ray*/ None,
|
||||
/*auth_error*/ None,
|
||||
/*auth_error_code*/ None,
|
||||
None,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"unknown",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
response
|
||||
@@ -400,6 +458,56 @@ impl SessionTelemetry {
|
||||
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,
|
||||
None,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"unknown",
|
||||
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,
|
||||
provider_header_names: Option<&str>,
|
||||
base_url_origin: &str,
|
||||
host_class: &str,
|
||||
base_url_source: &str,
|
||||
base_url_is_default: bool,
|
||||
request_id: Option<&str>,
|
||||
cf_ray: Option<&str>,
|
||||
auth_error: Option<&str>,
|
||||
auth_error_code: Option<&str>,
|
||||
error_body_class: Option<&str>,
|
||||
safe_error_message: Option<&str>,
|
||||
) {
|
||||
let success = status.is_some_and(|code| (200..=299).contains(&code)) && error.is_none();
|
||||
let success_str = if success { "true" } else { "false" };
|
||||
@@ -430,6 +538,12 @@ impl SessionTelemetry {
|
||||
auth.recovery_mode = recovery_mode,
|
||||
auth.recovery_phase = recovery_phase,
|
||||
endpoint = endpoint,
|
||||
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,
|
||||
base_url_source = base_url_source,
|
||||
base_url_is_default = base_url_is_default,
|
||||
auth.env_openai_api_key_present = self.metadata.auth_env.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.metadata.auth_env.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.metadata.auth_env.codex_api_key_env_enabled,
|
||||
@@ -440,6 +554,8 @@ impl SessionTelemetry {
|
||||
auth.cf_ray = cf_ray,
|
||||
auth.error = auth_error,
|
||||
auth.error_code = auth_error_code,
|
||||
error_body_class = error_body_class,
|
||||
safe_error_message = safe_error_message,
|
||||
},
|
||||
log: {},
|
||||
trace: {},
|
||||
@@ -463,6 +579,56 @@ impl SessionTelemetry {
|
||||
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,
|
||||
None,
|
||||
"unknown",
|
||||
"unknown",
|
||||
"unknown",
|
||||
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,
|
||||
provider_header_names: Option<&str>,
|
||||
base_url_origin: &str,
|
||||
host_class: &str,
|
||||
base_url_source: &str,
|
||||
base_url_is_default: bool,
|
||||
connection_reused: bool,
|
||||
request_id: Option<&str>,
|
||||
cf_ray: Option<&str>,
|
||||
auth_error: Option<&str>,
|
||||
auth_error_code: Option<&str>,
|
||||
error_body_class: Option<&str>,
|
||||
safe_error_message: Option<&str>,
|
||||
) {
|
||||
let success = error.is_none()
|
||||
&& status
|
||||
@@ -483,6 +649,12 @@ impl SessionTelemetry {
|
||||
auth.recovery_mode = recovery_mode,
|
||||
auth.recovery_phase = recovery_phase,
|
||||
endpoint = endpoint,
|
||||
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,
|
||||
base_url_source = base_url_source,
|
||||
base_url_is_default = base_url_is_default,
|
||||
auth.env_openai_api_key_present = self.metadata.auth_env.openai_api_key_env_present,
|
||||
auth.env_codex_api_key_present = self.metadata.auth_env.codex_api_key_env_present,
|
||||
auth.env_codex_api_key_enabled = self.metadata.auth_env.codex_api_key_env_enabled,
|
||||
@@ -494,6 +666,8 @@ impl SessionTelemetry {
|
||||
auth.cf_ray = cf_ray,
|
||||
auth.error = auth_error,
|
||||
auth.error_code = auth_error_code,
|
||||
error_body_class = error_body_class,
|
||||
safe_error_message = safe_error_message,
|
||||
},
|
||||
log: {},
|
||||
trace: {},
|
||||
|
||||
@@ -501,8 +501,12 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
.with_auth_env(auth_env_metadata());
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.conversation_starts(
|
||||
manager.conversation_starts_with_endpoint_details(
|
||||
"openai",
|
||||
"chatgpt.com",
|
||||
"openai_chatgpt",
|
||||
"default",
|
||||
true,
|
||||
None,
|
||||
ReasoningSummary::Auto,
|
||||
None,
|
||||
@@ -512,7 +516,7 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
Vec::new(),
|
||||
None,
|
||||
);
|
||||
manager.record_api_request(
|
||||
manager.record_api_request_with_endpoint_details(
|
||||
1,
|
||||
Some(401),
|
||||
Some("http 401"),
|
||||
@@ -523,10 +527,17 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
Some("managed"),
|
||||
Some("refresh_token"),
|
||||
"/responses",
|
||||
Some("openai-project"),
|
||||
"chatgpt.com",
|
||||
"openai_chatgpt",
|
||||
"default",
|
||||
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."),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -536,6 +547,34 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
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("client_origin")
|
||||
.map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_log_attrs
|
||||
.get("base_url_origin")
|
||||
.map(String::as_str),
|
||||
Some("chatgpt.com")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_log_attrs.get("host_class").map(String::as_str),
|
||||
Some("openai_chatgpt")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_log_attrs
|
||||
.get("base_url_source")
|
||||
.map(String::as_str),
|
||||
Some("default")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_log_attrs
|
||||
.get("base_url_is_default")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_log_attrs
|
||||
.get("auth.env_openai_api_key_present")
|
||||
@@ -550,6 +589,34 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
);
|
||||
let request_log = find_log_by_event_name(&logs, "codex.api_request");
|
||||
let request_log_attrs = log_attributes(&request_log.record);
|
||||
assert_eq!(
|
||||
request_log_attrs.get("client_origin").map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("openai-project")
|
||||
);
|
||||
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("default")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("base_url_is_default")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("auth.header_attached")
|
||||
@@ -584,6 +651,18 @@ 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("error_body_class")
|
||||
.map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs
|
||||
.get("safe_error_message")
|
||||
.map(String::as_str),
|
||||
Some("Workspace is not authorized in this region.")
|
||||
);
|
||||
assert_eq!(
|
||||
request_log_attrs.get("auth.error").map(String::as_str),
|
||||
Some("missing_authorization_header")
|
||||
@@ -611,9 +690,25 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
conversation_trace_attrs
|
||||
.get("base_url_origin")
|
||||
.map(String::as_str),
|
||||
Some("chatgpt.com")
|
||||
);
|
||||
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);
|
||||
assert_eq!(
|
||||
request_trace_attrs.get("client_origin").map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
assert_eq!(
|
||||
request_trace_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("openai-project")
|
||||
);
|
||||
assert_eq!(
|
||||
request_trace_attrs
|
||||
.get("auth.header_attached")
|
||||
@@ -642,6 +737,12 @@ fn otel_export_routing_policy_routes_api_request_auth_observability() {
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
request_trace_attrs
|
||||
.get("error_body_class")
|
||||
.map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -686,7 +787,7 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
.with_auth_env(auth_env_metadata());
|
||||
let root_span = tracing::info_span!("root");
|
||||
let _root_guard = root_span.enter();
|
||||
manager.record_websocket_connect(
|
||||
manager.record_websocket_connect_with_endpoint_details(
|
||||
std::time::Duration::from_millis(17),
|
||||
Some(401),
|
||||
Some("http 401"),
|
||||
@@ -696,11 +797,18 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
Some("managed"),
|
||||
Some("reload"),
|
||||
"/responses",
|
||||
Some("openai-project"),
|
||||
"chatgpt.com",
|
||||
"openai_chatgpt",
|
||||
"default",
|
||||
true,
|
||||
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."),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -710,6 +818,34 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
let logs = log_exporter.get_emitted_logs().expect("log export");
|
||||
let connect_log = find_log_by_event_name(&logs, "codex.websocket_connect");
|
||||
let connect_log_attrs = log_attributes(&connect_log.record);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("client_origin").map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("openai-project")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("base_url_origin").map(String::as_str),
|
||||
Some("chatgpt.com")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("host_class").map(String::as_str),
|
||||
Some("openai_chatgpt")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs.get("base_url_source").map(String::as_str),
|
||||
Some("default")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("base_url_is_default")
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("auth.header_attached")
|
||||
@@ -736,6 +872,18 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
.map(String::as_str),
|
||||
Some("false")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_log_attrs
|
||||
.get("error_body_class")
|
||||
.map(String::as_str),
|
||||
Some("workspace_not_authorized_in_region")
|
||||
);
|
||||
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.env_provider_key_name")
|
||||
@@ -747,6 +895,10 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
let connect_trace_event =
|
||||
find_span_event_by_name_attr(&spans[0].events.events, "codex.websocket_connect");
|
||||
let connect_trace_attrs = span_event_attributes(connect_trace_event);
|
||||
assert_eq!(
|
||||
connect_trace_attrs.get("client_origin").map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_trace_attrs
|
||||
.get("auth.recovery_phase")
|
||||
@@ -759,6 +911,12 @@ fn otel_export_routing_policy_routes_websocket_connect_auth_observability() {
|
||||
.map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
connect_trace_attrs
|
||||
.get("provider_header_names")
|
||||
.map(String::as_str),
|
||||
Some("openai-project")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user