mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Attribute command and plugin analytics to the invoking model (#45445)
## Why Command execution and plugin measurement events lack model and reasoning-effort labels. Attribution needs to reflect the step that invoked the command, even when model settings change before a background process finishes. ## What changed - Add `model_slug` and `reasoning_effort` to command execution and plugin measurement analytics. - Capture model context from resolved step settings and carry it through execution, approval, Guardian review, and plugin metrics collection. - Preserve the first command-start model context when subsequent start notifications arrive. - Keep the carried context out of serialized protocol items and generated schemas. ## Testing Extend analytics tests to cover model switches before invocation and during background execution, default reasoning effort, Guardian-denied commands, and repeated start notifications retaining the original model context. GitOrigin-RevId: af90e1c0d39bab625f2e89786085b61a9b96c0ce
This commit is contained in:
@@ -20,6 +20,7 @@ mod review_session;
|
||||
mod reviewer_config;
|
||||
mod runtime;
|
||||
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
@@ -82,6 +83,7 @@ pub(crate) struct GuardianReviewContext {
|
||||
pub(crate) parent_response_id: Option<String>,
|
||||
turn: Arc<TurnContext>,
|
||||
environments: TurnEnvironmentSnapshot,
|
||||
// Model and reasoning inputs are carried for the follow-up Guardian and V2 migrations.
|
||||
pub(crate) model_info: Arc<ModelInfo>,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffort>,
|
||||
pub(crate) reasoning_summary: ReasoningSummary,
|
||||
@@ -91,6 +93,17 @@ pub(crate) struct GuardianReviewContext {
|
||||
}
|
||||
|
||||
impl GuardianReviewContext {
|
||||
pub(crate) fn model_context(&self) -> ModelInvocationContext {
|
||||
ModelInvocationContext {
|
||||
model_slug: self.model_info.slug.clone(),
|
||||
reasoning_effort: self
|
||||
.reasoning_effort
|
||||
.as_ref()
|
||||
.or(self.model_info.default_reasoning_level.as_ref())
|
||||
.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_resolved_settings(
|
||||
turn: Arc<TurnContext>,
|
||||
settings: &ResolvedStepSettings,
|
||||
|
||||
@@ -44,6 +44,7 @@ impl ReviewHost for super::super::runtime::ReviewRuntime {
|
||||
Ok(request) => request.clone(),
|
||||
Err(decision) => return Err(decision),
|
||||
};
|
||||
let model_context = context.model_context();
|
||||
let turn = Arc::clone(context.turn());
|
||||
let GuardianReviewOptions {
|
||||
plugin_attribution_override,
|
||||
@@ -101,6 +102,7 @@ impl ReviewHost for super::super::runtime::ReviewRuntime {
|
||||
reviewed_action: guardian_reviewed_action(&request),
|
||||
action: guardian_assessment_action(&request),
|
||||
review_reason,
|
||||
model_context,
|
||||
});
|
||||
session
|
||||
.send_event(
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::tools::sandboxing::ToolCtx;
|
||||
use codex_analytics::PluginMeasurementsInput;
|
||||
use codex_core_plugins::PluginMetricsSidecar;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
/// Creates a metrics sidecar for one plugin command.
|
||||
@@ -34,6 +35,7 @@ pub(crate) async fn finish_and_track_measurements(
|
||||
exit_code: i32,
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
model_context: &ModelInvocationContext,
|
||||
item_id: &str,
|
||||
) {
|
||||
let Some(metrics_sidecar) = metrics_sidecar else {
|
||||
@@ -50,6 +52,8 @@ pub(crate) async fn finish_and_track_measurements(
|
||||
turn_id: turn.sub_id.clone(),
|
||||
item_id: item_id.to_string(),
|
||||
originator: turn.originator.clone(),
|
||||
model_slug: Some(model_context.model_slug.clone()),
|
||||
reasoning_effort: model_context.reasoning_effort.clone(),
|
||||
plugin_id: batch.plugin_id,
|
||||
execution_id: batch.execution_id,
|
||||
operation: batch.operation,
|
||||
|
||||
@@ -52,6 +52,8 @@ async fn command_approval_holds_an_elicitation_until_response() {
|
||||
.request_command_approval(
|
||||
turn_context.as_ref(),
|
||||
ExecApprovalKind::Command,
|
||||
crate::guardian::GuardianReviewContext::from(turn_context.clone())
|
||||
.model_context(),
|
||||
"call-1".to_string(),
|
||||
/*approval_id*/ None,
|
||||
/*environment_id*/ None,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::context::GuardianContextMode;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -23,6 +22,7 @@ use crate::config::ManagedFeatures;
|
||||
use crate::config::resolve_tool_suggest_config_from_layer_stack;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::DeveloperInstructions;
|
||||
use crate::context::GuardianContextMode;
|
||||
use crate::context::GuardianPolicy;
|
||||
use crate::context::ManagedDeveloperInstructions;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
@@ -107,6 +107,7 @@ use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::items::EnteredReviewModeItem;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
@@ -2790,6 +2791,7 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
kind: ExecApprovalKind,
|
||||
model_context: ModelInvocationContext,
|
||||
call_id: String,
|
||||
approval_id: Option<String>,
|
||||
environment_id: Option<String>,
|
||||
@@ -2853,6 +2855,7 @@ impl Session {
|
||||
.map(PluginCommandAttribution::serialized_fields)
|
||||
.unzip();
|
||||
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
model_context: Some(model_context),
|
||||
kind,
|
||||
call_id,
|
||||
plugin_id,
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
||||
use codex_exec_server::ResolvedSelectedCapabilityRoot;
|
||||
use codex_mcp::McpBinding;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
/// Request-scoped state that may change between model sampling requests.
|
||||
@@ -43,4 +44,14 @@ impl StepContext {
|
||||
item.summary = self.settings.reasoning_summary;
|
||||
item
|
||||
}
|
||||
|
||||
pub(crate) fn model_context(&self) -> ModelInvocationContext {
|
||||
ModelInvocationContext {
|
||||
model_slug: self.settings.model_info.slug.clone(),
|
||||
reasoning_effort: self
|
||||
.settings
|
||||
.effective_reasoning_effort()
|
||||
.map(|effort| effort.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_started(
|
||||
turn_context.as_ref(),
|
||||
&TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id.clone(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -271,6 +272,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_completed(
|
||||
turn_context.as_ref(),
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id,
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -296,6 +298,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_completed(
|
||||
turn_context.as_ref(),
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id.clone(),
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
@@ -341,6 +344,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
.emit_turn_item_completed(
|
||||
turn_context.as_ref(),
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
model_context: None,
|
||||
id: call_id,
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
|
||||
@@ -701,6 +701,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::Command,
|
||||
ctx.review_context.model_context(),
|
||||
ctx.call_id.clone(),
|
||||
/*approval_id*/ None,
|
||||
Some(environment_id.clone()),
|
||||
@@ -730,6 +731,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::WriteStdin,
|
||||
ctx.review_context.model_context(),
|
||||
id.clone(),
|
||||
Some(approval_id.clone()),
|
||||
Some(environment_id.clone()),
|
||||
@@ -761,6 +763,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::Command,
|
||||
ctx.review_context.model_context(),
|
||||
ctx.call_id.clone(),
|
||||
Some(approval_id.clone()),
|
||||
Some(environment_id.clone()),
|
||||
@@ -833,6 +836,7 @@ impl Session {
|
||||
self.request_command_approval(
|
||||
ctx.review_context.turn(),
|
||||
ExecApprovalKind::Command,
|
||||
ctx.review_context.model_context(),
|
||||
ctx.call_id.clone(),
|
||||
/*approval_id*/ None,
|
||||
Some(environment_id.clone()),
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::items::CommandExecutionItem;
|
||||
use codex_protocol::items::CommandExecutionStatus;
|
||||
use codex_protocol::items::FileChangeItem;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -43,6 +44,7 @@ pub(super) fn truncate_rejection_message(message: &str) -> String {
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ToolEventCtx<'a> {
|
||||
pub model_context: Option<&'a ModelInvocationContext>,
|
||||
pub session: &'a Session,
|
||||
pub turn: &'a TurnContext,
|
||||
/// Model captured by the step that issued this call, including delayed completion events.
|
||||
@@ -60,6 +62,7 @@ impl<'a> ToolEventCtx<'a> {
|
||||
turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,
|
||||
) -> Self {
|
||||
Self {
|
||||
model_context: None,
|
||||
session,
|
||||
turn,
|
||||
model_info,
|
||||
@@ -168,6 +171,7 @@ async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommand
|
||||
ctx.turn,
|
||||
&TurnItem::CommandExecution(CommandExecutionItem {
|
||||
id: ctx.call_id.to_string(),
|
||||
model_context: ctx.model_context.cloned(),
|
||||
plugin_id,
|
||||
script_path,
|
||||
process_id: exec_input.process_id.map(str::to_owned),
|
||||
@@ -581,6 +585,7 @@ async fn emit_exec_end(
|
||||
ctx.turn,
|
||||
TurnItem::CommandExecution(CommandExecutionItem {
|
||||
id: ctx.call_id.to_string(),
|
||||
model_context: ctx.model_context.cloned(),
|
||||
plugin_id,
|
||||
script_path,
|
||||
process_id: exec_input.process_id.map(str::to_owned),
|
||||
|
||||
@@ -178,6 +178,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
let session_ref = Arc::clone(&context.session);
|
||||
let turn_ref = Arc::clone(&context.step_context.turn);
|
||||
let model_info = Arc::clone(&context.step_context.settings.model_info);
|
||||
let model_context = context.step_context.model_context();
|
||||
let call_id = context.call_id.clone();
|
||||
let exit_token = process.cancellation_token();
|
||||
let output_drained = process.output_drained_notify();
|
||||
@@ -223,6 +224,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
exit_code,
|
||||
&session_ref,
|
||||
&turn_ref,
|
||||
&model_context,
|
||||
&call_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -276,11 +276,13 @@ struct InitialExecCommandGuard {
|
||||
|
||||
impl InitialExecCommandGuard {
|
||||
async fn finish_plugin_metrics(&mut self, context: &UnifiedExecContext, exit_code: i32) {
|
||||
let model_context = context.step_context.model_context();
|
||||
finish_and_track_measurements(
|
||||
self.metrics_sidecar.take(),
|
||||
exit_code,
|
||||
&context.session,
|
||||
&context.step_context.turn,
|
||||
&model_context,
|
||||
&context.call_id,
|
||||
)
|
||||
.await;
|
||||
@@ -530,13 +532,15 @@ impl UnifiedExecProcessManager {
|
||||
});
|
||||
|
||||
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
let model_context = context.step_context.model_context();
|
||||
let mut event_ctx = ToolEventCtx::new(
|
||||
context.session.as_ref(),
|
||||
context.step_context.turn.as_ref(),
|
||||
&context.step_context.settings.model_info,
|
||||
&context.call_id,
|
||||
/*turn_diff_tracker*/ None,
|
||||
);
|
||||
event_ctx.model_context = Some(&model_context);
|
||||
let plugin_attribution = if request.turn_environment.environment.is_remote() {
|
||||
let file_system = request.turn_environment.environment.get_filesystem();
|
||||
context
|
||||
@@ -727,6 +731,7 @@ impl UnifiedExecProcessManager {
|
||||
exit_code.unwrap_or(-1),
|
||||
&context.session,
|
||||
&context.step_context.turn,
|
||||
&model_context,
|
||||
&context.call_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user