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:
aojiao-oai
2026-11-03 02:26:29 +00:00
committed by copyberry
parent e839b828fb
commit 16ddd538bd
48 changed files with 443 additions and 39 deletions
+1
View File
@@ -2329,6 +2329,7 @@ mod tests {
.await
.expect("create paginated thread");
let paginated_item = RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "turn-1".to_string(),
item: TurnItem::UserMessage(UserMessageItem {
@@ -720,6 +720,7 @@ fn contextual_user_message() -> RolloutItem {
fn completed_user_message(turn_id: &str, message: &str) -> RolloutItem {
RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
content_token_count: None,
thread_id: codex_protocol::ThreadId::from_string("00000000-0000-0000-0000-000000000000")
.expect("fixture thread id"),
turn_id: turn_id.to_string(),
@@ -936,6 +936,7 @@ mod tests {
codex_rollout::append_rollout_item_to_path(
&rollout_path,
&RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "turn-1".to_string(),
item: TurnItem::UserMessage(UserMessageItem::new(&[UserInput::Text {
@@ -422,6 +422,7 @@ impl LegacyRolloutCanonicalizer {
writer,
timestamp,
RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
content_token_count: None,
thread_id: self.thread_id,
turn_id,
item,
@@ -222,6 +222,7 @@ fn exec_completion(turn_id: &str, call_id: &str, output: &str) -> RolloutItem {
fn item_completed(turn_id: &str, item_id: &str) -> RolloutItem {
RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
content_token_count: None,
thread_id: ThreadId::new(),
turn_id: turn_id.to_string(),
item: TurnItem::Reasoning(ReasoningItem {
@@ -29,6 +29,7 @@ async fn delegated_thread_is_listed_before_user_followup() {
live_thread
.append_items(&[RolloutItem::EventMsg(EventMsg::ItemCompleted(
ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "delegated-turn".to_string(),
item: TurnItem::FunctionCallOutput(FunctionCallOutputItem {
@@ -1573,6 +1573,7 @@ async fn projection_preserves_exact_lifecycle_timestamps() {
for (index, (started_at_ms, completed_at_ms)) in timestamps.iter().enumerate() {
items.push(RolloutItem::EventMsg(EventMsg::ItemCompleted(
ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "turn-1".to_string(),
item: TurnItem::UserMessage(UserMessageItem {
@@ -3171,6 +3172,7 @@ fn contains_user_message(items: &[RolloutItem], expected: &str) -> bool {
fn completed_item(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,
@@ -538,6 +538,7 @@ mod tests {
thread_id,
vec![RolloutItem::EventMsg(EventMsg::ItemCompleted(
ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "turn-1".to_string(),
item: TurnItem::UserMessage(UserMessageItem::new(&[UserInput::Text {
@@ -650,6 +651,7 @@ mod tests {
let update = sync
.observe_appended_items(&[RolloutItem::EventMsg(EventMsg::ItemCompleted(
ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "turn-1".to_string(),
item: TurnItem::UserMessage(UserMessageItem::new(&[UserInput::Text {
@@ -27,6 +27,7 @@ fn delegated_output_emits_only_first_preview_in_live_patch() {
);
for (index, text) in ["first task", "later task"].into_iter().enumerate() {
let item = RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
content_token_count: None,
thread_id,
turn_id: "turn".to_string(),
item: TurnItem::FunctionCallOutput(FunctionCallOutputItem {