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:
@@ -1009,6 +1009,13 @@ pub struct InternalChatMessageMetadataPassthrough {
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub create_time: Option<serde_json::Number>,
|
||||
/// Native content-token count for this sampled message, including its sampled footer.
|
||||
/// Read from the producer only; do not replay telemetry into subsequent model requests.
|
||||
#[serde_as(deserialize_as = "serde_with::DefaultOnError")]
|
||||
#[serde(default, skip_serializing)]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub content_token_count: Option<u64>,
|
||||
/// Harness-owned classifications aligned with the item's content entries.
|
||||
#[serde_as(deserialize_as = "serde_with::DefaultOnError")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -57,6 +57,41 @@ fn malformed_content_kinds_do_not_prevent_loading_response_item() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_content_token_counts_do_not_prevent_loading_response_item() -> Result<()> {
|
||||
for value in [json!(-1), json!(true), json!(1.5), json!("4"), json!(null)] {
|
||||
let item: ResponseItem = serde_json::from_value(response_item_json(json!({
|
||||
"content_token_count": value,
|
||||
})))?;
|
||||
assert_eq!(
|
||||
item,
|
||||
response_item(InternalChatMessageMetadataPassthrough::default())
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_token_counts_are_not_replayed_into_requests() -> Result<()> {
|
||||
let item: ResponseItem = serde_json::from_value(response_item_json(json!({
|
||||
"turn_id": "turn-1", "content_token_count": 23,
|
||||
})))?;
|
||||
let ResponseItem::Message {
|
||||
internal_chat_message_metadata_passthrough: Some(ref metadata),
|
||||
..
|
||||
} = item
|
||||
else {
|
||||
panic!("expected message metadata");
|
||||
};
|
||||
assert_eq!(metadata.content_token_count, Some(23));
|
||||
let encoded = serde_json::to_value(item)?;
|
||||
assert_eq!(
|
||||
encoded["internal_chat_message_metadata_passthrough"],
|
||||
json!({"turn_id": "turn-1"})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn response_item(metadata: InternalChatMessageMetadataPassthrough) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
status: None,
|
||||
|
||||
@@ -2194,6 +2194,11 @@ pub struct ItemCompletedEvent {
|
||||
pub thread_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub item: TurnItem,
|
||||
/// Native tokens in the completed assistant message, including any sampled footer.
|
||||
/// Completion telemetry only; absent for items without a producer-supplied count.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub content_token_count: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub started_at_ms: Option<i64>,
|
||||
@@ -5763,6 +5768,7 @@ mod tests {
|
||||
#[test]
|
||||
fn item_completed_event_from_image_generation_emits_end_event() {
|
||||
let event = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
item: TurnItem::ImageGeneration(ImageGenerationItem {
|
||||
@@ -5801,6 +5807,7 @@ mod tests {
|
||||
#[test]
|
||||
fn item_completed_event_from_file_change_emits_patch_end_event() {
|
||||
let event = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: Some(0),
|
||||
@@ -5840,6 +5847,7 @@ mod tests {
|
||||
#[test]
|
||||
fn item_completed_event_from_mcp_tool_call_emits_end_event() {
|
||||
let event = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: Some(0),
|
||||
@@ -5921,6 +5929,7 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
let completed = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: Some(10),
|
||||
@@ -5999,6 +6008,7 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
let completed = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: Some(10),
|
||||
@@ -6044,6 +6054,7 @@ mod tests {
|
||||
#[test]
|
||||
fn review_mode_item_completion_emits_legacy_events_with_ids() {
|
||||
let entered = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: Some(0),
|
||||
@@ -6057,6 +6068,7 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
let exited = ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: Some(0),
|
||||
@@ -6117,6 +6129,7 @@ mod tests {
|
||||
#[test]
|
||||
fn item_completed_event_defaults_missing_completed_at_ms() {
|
||||
let mut value = serde_json::to_value(ItemCompletedEvent {
|
||||
content_token_count: None,
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
item: TurnItem::UserMessage(UserMessageItem::new(&[])),
|
||||
|
||||
Reference in New Issue
Block a user