mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Expose assistant content token counts in completion notifications (#59041)
## What changed Add optional `contentTokenCount` metadata to `item/completed` notifications, carrying the producer-supplied token count for each completed assistant message in default and plan modes. Counts include any sampled footer and are `null` when unavailable. Keep this telemetry separate from stored thread items and omit it from subsequent model requests. Update the protocol schemas and generated TypeScript and Python types. ## Testing Add coverage for per-message counts, including zero, in default and plan modes; missing and malformed metadata; backward-compatible notification decoding; and exclusion from subsequent model requests. GitOrigin-RevId: 35973037a42650fa2d7569da8e88cb81215cf971
This commit is contained in:
@@ -419,13 +419,15 @@ pub fn item_event_to_server_notification(
|
||||
))
|
||||
}
|
||||
EventMsg::ItemCompleted(item_completed_event) => {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
let mut notification = ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
ThreadItemPayload::from(item_completed_event.item),
|
||||
item_completed_event.started_at_ms,
|
||||
item_completed_event.completed_at_ms,
|
||||
))
|
||||
);
|
||||
notification.content_token_count = item_completed_event.content_token_count;
|
||||
ServerNotification::ItemCompleted(notification)
|
||||
}
|
||||
EventMsg::PatchApplyUpdated(event) => {
|
||||
ServerNotification::FileChangePatchUpdated(FileChangePatchUpdatedNotification {
|
||||
@@ -487,6 +489,56 @@ mod tests {
|
||||
use codex_protocol::protocol::ExecOutputStream;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn assistant_completion_count_is_optional_envelope_metadata() {
|
||||
let item = codex_protocol::items::TurnItem::AgentMessage(
|
||||
codex_protocol::items::AgentMessageItem {
|
||||
id: "answer".into(),
|
||||
content: vec![codex_protocol::items::AgentMessageContent::Text {
|
||||
text: "Answer".into(),
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
);
|
||||
let payload = ThreadItemPayload::from(item.clone());
|
||||
for count in [None, Some(0), Some(23)] {
|
||||
let event = codex_protocol::protocol::ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn".into(),
|
||||
item: item.clone(),
|
||||
content_token_count: count,
|
||||
started_at_ms: Some(1),
|
||||
completed_at_ms: 2,
|
||||
};
|
||||
let mut expected = ItemCompletedNotification::new(
|
||||
"thread".into(),
|
||||
"turn".into(),
|
||||
payload.clone(),
|
||||
/*started_at_ms*/ Some(1),
|
||||
/*completed_at_ms*/ 2,
|
||||
);
|
||||
expected.content_token_count = count;
|
||||
assert_item_completed_server_notification(
|
||||
item_event_to_server_notification(EventMsg::ItemCompleted(event), "thread", "turn"),
|
||||
expected.clone(),
|
||||
);
|
||||
let mut wire = serde_json::to_value(&expected).unwrap();
|
||||
assert_eq!(
|
||||
wire.get("contentTokenCount"),
|
||||
Some(&serde_json::json!(count))
|
||||
);
|
||||
wire.as_object_mut().unwrap().remove("contentTokenCount");
|
||||
expected.content_token_count = None;
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ItemCompletedNotification>(wire).unwrap(),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_item_started_server_notification(
|
||||
notification: ServerNotification,
|
||||
expected: ItemStartedNotification,
|
||||
|
||||
@@ -2135,6 +2135,7 @@ mod tests {
|
||||
};
|
||||
builder.handle_event_for_history_mode(&EventMsg::ItemStarted(started), mode);
|
||||
let completed = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::default(),
|
||||
turn_id: turn_id.clone(),
|
||||
item,
|
||||
@@ -2336,6 +2337,7 @@ mod tests {
|
||||
});
|
||||
let events = vec![
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id,
|
||||
turn_id: "turn-1".into(),
|
||||
item: entered,
|
||||
@@ -2343,6 +2345,7 @@ mod tests {
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id,
|
||||
turn_id: "turn-1".into(),
|
||||
item: exited,
|
||||
@@ -2594,6 +2597,7 @@ mod tests {
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id,
|
||||
turn_id: turn_id.to_string(),
|
||||
item: sleep_item,
|
||||
@@ -2679,6 +2683,7 @@ mod tests {
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id,
|
||||
turn_id: turn_id.to_string(),
|
||||
item: CoreTurnItem::Extension(CoreExtensionItem::ImageGeneration(
|
||||
@@ -2797,6 +2802,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id,
|
||||
turn_id: turn_id.to_string(),
|
||||
item: command_item,
|
||||
@@ -4073,6 +4079,7 @@ mod tests {
|
||||
use codex_protocol::protocol::HasLegacyEvent;
|
||||
|
||||
let completion = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-a".into(),
|
||||
started_at_ms: None,
|
||||
@@ -4388,6 +4395,7 @@ mod tests {
|
||||
("c", "latest"),
|
||||
] {
|
||||
builder.handle_live_paginated_event(&EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::default(),
|
||||
turn_id: turn_id.clone(),
|
||||
item: CoreTurnItem::AgentMessage(codex_protocol::items::AgentMessageItem {
|
||||
@@ -5530,6 +5538,7 @@ mod tests {
|
||||
}));
|
||||
let changes =
|
||||
builder.handle_event_with_changes(&EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-a".into(),
|
||||
item: hook_prompt,
|
||||
@@ -5551,6 +5560,7 @@ mod tests {
|
||||
.join("worker")
|
||||
.expect("worker path");
|
||||
let spawn = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-a".into(),
|
||||
item: CoreTurnItem::SubAgentActivity(CoreSubAgentActivityItem {
|
||||
@@ -5601,6 +5611,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-a".into(),
|
||||
item: CoreTurnItem::SubAgentActivity(CoreSubAgentActivityItem {
|
||||
|
||||
@@ -178,6 +178,7 @@ fn projects_optional_completed_item_lifecycle_timestamps() {
|
||||
{
|
||||
let changes = project(RolloutItem::EventMsg(EventMsg::ItemCompleted(
|
||||
ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::default(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: item.clone(),
|
||||
@@ -283,6 +284,7 @@ fn project(item: RolloutItem) -> ThreadHistoryChangeSet {
|
||||
|
||||
fn item_completed(thread_id: ThreadId, turn_id: &str, item: TurnItem) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id,
|
||||
turn_id: turn_id.to_string(),
|
||||
item,
|
||||
|
||||
@@ -1505,6 +1505,12 @@ pub struct ItemCompletedNotification {
|
||||
pub item: ThreadItem,
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
/// Native content tokens for the completed assistant message, including a sampled footer.
|
||||
/// Null when unavailable. Completion telemetry is not part of the stored thread item.
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
#[ts(type = "number | null")]
|
||||
pub content_token_count: Option<u64>,
|
||||
/// Unix timestamp (in milliseconds) when this item lifecycle completed.
|
||||
#[ts(type = "number")]
|
||||
pub completed_at_ms: i64,
|
||||
@@ -1520,6 +1526,7 @@ impl ItemCompletedNotification {
|
||||
Self {
|
||||
thread_id,
|
||||
turn_id,
|
||||
content_token_count: None,
|
||||
completed_at_ms,
|
||||
item: ThreadItem::completed(payload, started_at_ms, completed_at_ms),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user