Compare commits

...

4 Commits

Author SHA1 Message Date
Ahmed Ibrahim
8fef273391 codex: address PR review feedback (#23734) 2026-05-21 14:09:51 -07:00
Ahmed Ibrahim
1b71e4be15 Merge branch 'main' into dev/add-turn-plan-type-telemetry 2026-05-21 13:50:08 -07:00
Ahmed Ibrahim
6c294f0f01 Avoid resolving auth for turn plan telemetry 2026-05-21 13:31:08 -07:00
Ahmed Ibrahim
4b9347fad4 Add plan type to turn telemetry 2026-05-20 12:20:19 -07:00
6 changed files with 158 additions and 0 deletions

View File

@@ -129,6 +129,7 @@ use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginId;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ModeKind;
@@ -371,6 +372,7 @@ fn sample_turn_resolved_config(thread_id: &str, turn_id: &str) -> TurnResolvedCo
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
plan_type: Some(AccountPlanType::Pro),
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
sandbox_network_access: true,
@@ -3197,6 +3199,7 @@ fn turn_event_serializes_expected_shape() {
reasoning_effort: Some("high".to_string()),
reasoning_summary: Some("detailed".to_string()),
service_tier: "flex".to_string(),
plan_type: None,
approval_policy: "on-request".to_string(),
approvals_reviewer: "auto_review".to_string(),
sandbox_network_access: true,
@@ -3258,6 +3261,7 @@ fn turn_event_serializes_expected_shape() {
"reasoning_effort": "high",
"reasoning_summary": "detailed",
"service_tier": "flex",
"plan_type": null,
"approval_policy": "on-request",
"approvals_reviewer": "auto_review",
"sandbox_network_access": true,
@@ -3572,6 +3576,7 @@ async fn turn_lifecycle_emits_turn_event() {
);
assert!(payload["event_params"].get("product_client_id").is_none());
assert_eq!(payload["event_params"]["ephemeral"], json!(false));
assert_eq!(payload["event_params"]["plan_type"], json!("pro"));
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
assert_eq!(payload["event_params"]["status"], json!("completed"));
assert_eq!(payload["event_params"]["steer_count"], json!(0));

View File

@@ -24,6 +24,7 @@ use codex_app_server_protocol::CodexErrorInfo;
use codex_app_server_protocol::CommandExecutionSource;
use codex_login::default_client::originator;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::SandboxPermissions;
@@ -784,6 +785,7 @@ pub(crate) struct CodexTurnEventParams {
pub(crate) reasoning_effort: Option<String>,
pub(crate) reasoning_summary: Option<String>,
pub(crate) service_tier: String,
pub(crate) plan_type: Option<AccountPlanType>,
pub(crate) approval_policy: String,
pub(crate) approvals_reviewer: String,
pub(crate) sandbox_network_access: bool,

View File

@@ -10,6 +10,7 @@ use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ServerResponse;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
@@ -76,6 +77,7 @@ pub struct TurnResolvedConfigFact {
pub reasoning_effort: Option<ReasoningEffort>,
pub reasoning_summary: Option<ReasoningSummary>,
pub service_tier: Option<ServiceTier>,
pub plan_type: Option<AccountPlanType>,
pub approval_policy: AskForApproval,
pub approvals_reviewer: ApprovalsReviewer,
pub sandbox_network_access: bool,

View File

@@ -2437,6 +2437,7 @@ fn codex_turn_event_params(
reasoning_effort,
reasoning_summary,
service_tier,
plan_type,
approval_policy,
approvals_reviewer,
sandbox_network_access,
@@ -2467,6 +2468,7 @@ fn codex_turn_event_params(
service_tier: service_tier
.map(|value| value.to_string())
.unwrap_or_else(|| "default".to_string()),
plan_type,
approval_policy: approval_policy.to_string(),
approvals_reviewer: approvals_reviewer.to_string(),
sandbox_network_access,

View File

@@ -70,6 +70,7 @@ use codex_async_utils::OrCancelExt;
use codex_features::Feature;
use codex_git_utils::get_git_repo_root;
use codex_git_utils::get_git_repo_root_with_fs;
use codex_protocol::account::ProviderAccount;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ServiceTier;
@@ -599,6 +600,15 @@ async fn track_turn_resolved_config_analytics(
let mut state = sess.state.lock().await;
state.take_next_turn_is_first()
};
let plan_type = match turn_context
.provider
.account_state()
.ok()
.and_then(|state| state.account)
{
Some(ProviderAccount::Chatgpt { plan_type, .. }) => Some(plan_type),
Some(ProviderAccount::ApiKey) | Some(ProviderAccount::AmazonBedrock) | None => None,
};
sess.services
.analytics_events_client
.track_turn_resolved_config(TurnResolvedConfigFact {
@@ -625,6 +635,7 @@ async fn track_turn_resolved_config_analytics(
.service_tier
.as_deref()
.and_then(ServiceTier::from_request_value),
plan_type,
approval_policy: turn_context.approval_policy.value(),
approvals_reviewer: turn_context.config.approvals_reviewer,
sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(),

View File

@@ -74,6 +74,8 @@ use serde_json::json;
use std::io::Write;
use std::num::NonZeroU64;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use tempfile::TempDir;
use toml::toml;
use uuid::Uuid;
@@ -3028,6 +3030,140 @@ fn create_dummy_codex_auth() -> CodexAuth {
CodexAuth::create_dummy_chatgpt_auth_for_testing()
}
async fn create_chatgpt_auth_with_plan(
plan_type: &str,
chatgpt_base_url: &str,
) -> anyhow::Result<(TempDir, CodexAuth)> {
let codex_home = TempDir::new()?;
write_auth_json(
&codex_home,
/*openai_api_key*/ None,
plan_type,
"Access-123",
Some("acc-123"),
);
let auth = CodexAuth::from_auth_storage(
codex_home.path(),
AuthCredentialsStoreMode::File,
Some(chatgpt_base_url),
)
.await?
.expect("auth.json should load ChatGPT auth");
Ok((codex_home, auth))
}
async fn submit_hello_turn(codex: &codex_core::CodexThread) -> anyhow::Result<()> {
codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
wait_for_event(codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
Ok(())
}
async fn wait_for_turn_analytics_event(server: &MockServer) -> serde_json::Value {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let requests = server.received_requests().await.unwrap_or_default();
if let Some(event) = requests
.into_iter()
.filter(|request| request.url.path() == "/codex/analytics-events/events")
.find_map(|request| {
let payload: serde_json::Value = serde_json::from_slice(&request.body).ok()?;
payload["events"].as_array().and_then(|events| {
events
.iter()
.find(|event| event["event_type"] == "codex_turn_event")
.cloned()
})
})
{
return event;
}
if Instant::now() >= deadline {
panic!("timed out waiting for turn analytics request");
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn turn_analytics_uses_authenticated_provider_plan() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
let _resp_mock = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let chatgpt_base_url = server.uri();
let (_auth_home, auth) = create_chatgpt_auth_with_plan("pro", &chatgpt_base_url).await?;
let mut builder = test_codex()
.with_auth(auth)
.with_config(move |config| config.chatgpt_base_url = chatgpt_base_url);
let codex = builder.build(&server).await?.codex;
submit_hello_turn(&codex).await?;
let event = wait_for_turn_analytics_event(&server).await;
assert_eq!(event["event_params"]["plan_type"], json!("pro"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn turn_analytics_omits_plan_for_provider_without_openai_auth() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
let _resp_mock = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let provider = ModelProviderInfo {
name: "custom".to_string(),
base_url: Some(format!("{}/v1", server.uri())),
env_key: None,
experimental_bearer_token: None,
auth: None,
aws: None,
query_params: None,
env_key_instructions: None,
wire_api: WireApi::Responses,
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: false,
supports_websockets: false,
};
let chatgpt_base_url = server.uri();
let (_auth_home, auth) = create_chatgpt_auth_with_plan("pro", &chatgpt_base_url).await?;
let mut builder = test_codex().with_auth(auth).with_config(move |config| {
config.model_provider = provider;
config.model_provider_id = "custom".to_string();
config.chatgpt_base_url = chatgpt_base_url;
});
let codex = builder.build(&server).await?.codex;
submit_hello_turn(&codex).await?;
let event = wait_for_turn_analytics_event(&server).await;
assert_eq!(event["event_params"]["plan_type"], json!(null));
Ok(())
}
/// Scenario:
/// - Turn 1: user sends U1; model streams deltas then a final assistant message A.
/// - Turn 2: user sends U2; model streams a delta then the same final assistant message A.