mirror of
https://github.com/openai/codex.git
synced 2026-11-11 14:32:25 +00:00
Preserve raw response usage metadata (#41980)
## What changed - Include the complete upstream `response.usage` object in `ResponseUsageMetadata.metadata` while retaining the existing `amount` field. - Expose the metadata through `rawResponse/completed` notifications and update the generated JSON and TypeScript schemas. ## Testing - Cover SSE, WebSocket, turn, and compaction completion paths. GitOrigin-RevId: 6d79b1b4c92ac11abf43ec98f380b4868288a701
This commit is contained in:
1 parent
d038f3448f
commit
e017e93ace
14 files changed
+62
-13
No files matched your search
+2
-1
@@ -18732,7 +18732,8 @@
|
|||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"metadata": true
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -14804,7 +14804,8 @@
|
|||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"metadata": true
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,8 @@
|
|||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"metadata": true
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { JsonValue } from "../serde_json/JsonValue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Usage metadata reported for one upstream response.
|
* Usage metadata reported for one upstream response.
|
||||||
*/
|
*/
|
||||||
export type ResponseUsageMetadata = { amount: string | null, };
|
export type ResponseUsageMetadata = { amount: string | null, metadata: JsonValue | null, };
|
||||||
@@ -1856,12 +1856,14 @@ pub struct RawResponseCompletedNotification {
|
|||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ResponseUsageMetadata {
|
pub struct ResponseUsageMetadata {
|
||||||
pub amount: Option<String>,
|
pub amount: Option<String>,
|
||||||
|
pub metadata: Option<JsonValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<codex_protocol::ResponseUsageMetadata> for ResponseUsageMetadata {
|
impl From<codex_protocol::ResponseUsageMetadata> for ResponseUsageMetadata {
|
||||||
fn from(value: codex_protocol::ResponseUsageMetadata) -> Self {
|
fn from(value: codex_protocol::ResponseUsageMetadata) -> Self {
|
||||||
Self {
|
Self {
|
||||||
amount: value.amount,
|
amount: value.amount,
|
||||||
|
metadata: value.metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
|
|||||||
]);
|
]);
|
||||||
let mut completed = responses::ev_completed_with_tokens("r1", /*total_tokens*/ 200);
|
let mut completed = responses::ev_completed_with_tokens("r1", /*total_tokens*/ 200);
|
||||||
completed["response"]["usage_metadata"] = serde_json::json!({ "amount": "0.125" });
|
completed["response"]["usage_metadata"] = serde_json::json!({ "amount": "0.125" });
|
||||||
|
completed["response"]["usage"]["extra"] = serde_json::json!({ "label": "example" });
|
||||||
|
let expected_metadata = completed["response"]["usage"].clone();
|
||||||
let sse = responses::sse(vec![
|
let sse = responses::sse(vec![
|
||||||
responses::ev_assistant_message("m1", "MANUAL_COMPACT_SUMMARY"),
|
responses::ev_assistant_message("m1", "MANUAL_COMPACT_SUMMARY"),
|
||||||
completed,
|
completed,
|
||||||
@@ -338,6 +340,7 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
|
|||||||
response_id: "r1".to_string(),
|
response_id: "r1".to_string(),
|
||||||
usage_metadata: Some(ResponseUsageMetadata {
|
usage_metadata: Some(ResponseUsageMetadata {
|
||||||
amount: Some("0.125".to_string()),
|
amount: Some("0.125".to_string()),
|
||||||
|
metadata: Some(expected_metadata),
|
||||||
}),
|
}),
|
||||||
usage: Some(TokenUsageBreakdown {
|
usage: Some(TokenUsageBreakdown {
|
||||||
total_tokens: 200,
|
total_tokens: 200,
|
||||||
|
|||||||
@@ -904,19 +904,24 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_case(None, json!(null); "without_usage_metadata")]
|
#[test_case(None, None, None; "without_usage_metadata")]
|
||||||
#[test_case(Some(json!({})), json!({ "amount": null }); "without_amount")]
|
#[test_case(Some(json!({})), None, None; "without_amount")]
|
||||||
#[test_case(Some(json!({ "amount": null })), json!({ "amount": null }); "null_amount")]
|
#[test_case(Some(json!({ "amount": null })), None, None; "null_amount")]
|
||||||
#[test_case(Some(json!({ "amount": "0" })), json!({ "amount": "0" }); "zero_amount")]
|
#[test_case(Some(json!({ "amount": "0" })), Some("0"), None; "zero_amount")]
|
||||||
#[test_case(
|
#[test_case(
|
||||||
Some(json!({ "amount": "0.12345678901234567890" })),
|
Some(json!({ "amount": "0.12345678901234567890" })),
|
||||||
json!({ "amount": "0.12345678901234567890" });
|
Some("0.12345678901234567890"),
|
||||||
|
Some(json!({ "label": "example", "items": [0, null, true] }));
|
||||||
"exact_amount"
|
"exact_amount"
|
||||||
)]
|
)]
|
||||||
|
#[test_case(None, None, Some(json!(null)); "null_extra")]
|
||||||
|
#[test_case(None, None, Some(json!(0)); "zero_extra")]
|
||||||
|
#[test_case(None, None, Some(json!({ "label": "example" })); "nested_extra")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn turn_start_emits_raw_response_completed_with_upstream_usage(
|
async fn turn_start_emits_raw_response_completed_with_upstream_usage(
|
||||||
upstream_metadata: Option<Value>,
|
upstream_metadata: Option<Value>,
|
||||||
expected_metadata: Value,
|
expected_amount: Option<&str>,
|
||||||
|
extra: Option<Value>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let server = responses::start_mock_server().await;
|
let server = responses::start_mock_server().await;
|
||||||
let mut completed = json!({
|
let mut completed = json!({
|
||||||
@@ -935,6 +940,13 @@ async fn turn_start_emits_raw_response_completed_with_upstream_usage(
|
|||||||
if let Some(metadata) = upstream_metadata {
|
if let Some(metadata) = upstream_metadata {
|
||||||
completed["response"]["usage_metadata"] = metadata;
|
completed["response"]["usage_metadata"] = metadata;
|
||||||
}
|
}
|
||||||
|
if let Some(extra) = extra {
|
||||||
|
completed["response"]["usage"]["extra"] = extra;
|
||||||
|
}
|
||||||
|
let expected_metadata = json!({
|
||||||
|
"amount": expected_amount,
|
||||||
|
"metadata": completed["response"]["usage"],
|
||||||
|
});
|
||||||
let body = responses::sse(vec![
|
let body = responses::sse(vec![
|
||||||
responses::ev_response_created("resp-1"),
|
responses::ev_response_created("resp-1"),
|
||||||
responses::ev_assistant_message("msg-1", "Done"),
|
responses::ev_assistant_message("msg-1", "Done"),
|
||||||
|
|||||||
@@ -478,8 +478,15 @@ pub fn process_responses_event(
|
|||||||
}
|
}
|
||||||
"response.completed" => {
|
"response.completed" => {
|
||||||
if let Some(resp_val) = event.response {
|
if let Some(resp_val) = event.response {
|
||||||
|
let metadata = resp_val
|
||||||
|
.get("usage")
|
||||||
|
.filter(|usage| !usage.is_null())
|
||||||
|
.cloned();
|
||||||
match serde_json::from_value::<ResponseCompleted>(resp_val) {
|
match serde_json::from_value::<ResponseCompleted>(resp_val) {
|
||||||
Ok(resp) => {
|
Ok(mut resp) => {
|
||||||
|
if let Some(metadata) = metadata {
|
||||||
|
resp.usage_metadata.get_or_insert_default().metadata = Some(metadata);
|
||||||
|
}
|
||||||
return Ok(Some(ResponseEvent::Completed {
|
return Ok(Some(ResponseEvent::Completed {
|
||||||
response_id: resp.id,
|
response_id: resp.id,
|
||||||
token_usage: resp.usage.map(Into::into),
|
token_usage: resp.usage.map(Into::into),
|
||||||
|
|||||||
@@ -118,11 +118,13 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()>
|
|||||||
"input_tokens": 10,
|
"input_tokens": 10,
|
||||||
"output_tokens": 5,
|
"output_tokens": 5,
|
||||||
"total_tokens": 15,
|
"total_tokens": 15,
|
||||||
|
"extra": { "label": "example", "items": [0, null, true] },
|
||||||
"codex_rollout_budget_units": 2.5
|
"codex_rollout_budget_units": 2.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let expected_metadata = completed["response"]["usage"].clone();
|
||||||
let body = build_responses_body(vec![item1, item2, completed]);
|
let body = build_responses_body(vec![item1, item2, completed]);
|
||||||
let transport = FixtureSseTransport::new(body);
|
let transport = FixtureSseTransport::new(body);
|
||||||
let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
|
let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
|
||||||
@@ -174,6 +176,7 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()>
|
|||||||
usage_metadata,
|
usage_metadata,
|
||||||
&Some(codex_protocol::ResponseUsageMetadata {
|
&Some(codex_protocol::ResponseUsageMetadata {
|
||||||
amount: Some("0.12345678901234567890".to_string()),
|
amount: Some("0.12345678901234567890".to_string()),
|
||||||
|
metadata: Some(expected_metadata),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -1175,18 +1175,32 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
usage_metadata: Some(codex_protocol::ResponseUsageMetadata {
|
usage_metadata: Some(codex_protocol::ResponseUsageMetadata {
|
||||||
amount: Some("0.125".to_string()),
|
amount: Some("0.125".to_string()),
|
||||||
|
metadata: Some(serde_json::json!({ "extra": { "label": "example" } })),
|
||||||
}),
|
}),
|
||||||
end_turn: Some(true),
|
end_turn: Some(true),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let (sess, turn_context) = crate::session::tests::make_session_and_context().await;
|
let (sess, turn_context, rx) =
|
||||||
|
crate::session::tests::make_session_and_context_with_rx().await;
|
||||||
let output = collect_compaction_output(&sess, &turn_context, stream)
|
let output = collect_compaction_output(&sess, &turn_context, stream)
|
||||||
.await
|
.await
|
||||||
.expect("compaction should be collected");
|
.expect("compaction should be collected");
|
||||||
|
|
||||||
assert_eq!(output.compaction_output, compaction);
|
assert_eq!(output.compaction_output, compaction);
|
||||||
assert_eq!(output.response_id, "resp-compact");
|
assert_eq!(output.response_id, "resp-compact");
|
||||||
|
let event = rx.recv().await.expect("raw response completion");
|
||||||
|
let EventMsg::RawResponseCompleted(completed) = event.msg else {
|
||||||
|
panic!("expected raw response completion, got {:?}", event.msg);
|
||||||
|
};
|
||||||
|
assert_eq!(completed.response_id, "resp-compact");
|
||||||
|
assert_eq!(
|
||||||
|
completed.usage_metadata,
|
||||||
|
Some(codex_protocol::ResponseUsageMetadata {
|
||||||
|
amount: Some("0.125".to_string()),
|
||||||
|
metadata: Some(serde_json::json!({ "extra": { "label": "example" } })),
|
||||||
|
})
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
output.token_usage,
|
output.token_usage,
|
||||||
Some(TokenUsage {
|
Some(TokenUsage {
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ async fn responses_websocket_preserves_credit_usage_metadata() {
|
|||||||
|
|
||||||
let mut completed = ev_completed("resp-1");
|
let mut completed = ev_completed("resp-1");
|
||||||
completed["response"]["usage_metadata"] = json!({ "amount": "0.12345678901234567890" });
|
completed["response"]["usage_metadata"] = json!({ "amount": "0.12345678901234567890" });
|
||||||
|
let expected_metadata = completed["response"]["usage"].clone();
|
||||||
let server =
|
let server =
|
||||||
start_websocket_server(vec![vec![vec![ev_response_created("resp-1"), completed]]]).await;
|
start_websocket_server(vec![vec![vec![ev_response_created("resp-1"), completed]]]).await;
|
||||||
let harness = websocket_harness_for_codex_backend(&server).await;
|
let harness = websocket_harness_for_codex_backend(&server).await;
|
||||||
@@ -193,6 +194,7 @@ async fn responses_websocket_preserves_credit_usage_metadata() {
|
|||||||
usage_metadata,
|
usage_metadata,
|
||||||
Some(codex_protocol::ResponseUsageMetadata {
|
Some(codex_protocol::ResponseUsageMetadata {
|
||||||
amount: Some("0.12345678901234567890".to_string()),
|
amount: Some("0.12345678901234567890".to_string()),
|
||||||
|
metadata: Some(expected_metadata),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(server.single_connection().len(), 1);
|
assert_eq!(server.single_connection().len(), 1);
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
/// Usage metadata reported for one upstream response.
|
/// Usage metadata reported for one upstream response.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
|
||||||
pub struct ResponseUsageMetadata {
|
pub struct ResponseUsageMetadata {
|
||||||
pub amount: Option<String>,
|
pub amount: Option<String>,
|
||||||
|
pub metadata: Option<Value>,
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user