mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Preserve native USD amounts in thread usage responses (#50442)
## What changed Add optional `native_usage_usd_micros` fields to thread usage totals and breakdown groups, and carry backend values through the client alongside existing credit and USD estimates. Missing or null native amounts remain `None`, while explicit zero amounts remain `Some(0)`. ## Testing Add coverage for native totals and group amounts, mixed batches with legacy responses, and the distinction between missing, null, and zero native group amounts. GitOrigin-RevId: 53891fc95807138e2eccb99d4cda1520b857dba4
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! Authoritative estimated credit and dollar usage for bounded batches of Codex threads.
|
||||
//! Backend credit estimates and native-dollar usage for bounded batches of Codex threads.
|
||||
|
||||
use super::Client;
|
||||
use super::PathStyle;
|
||||
@@ -17,6 +17,7 @@ pub struct ThreadUsageBreakdownGroup {
|
||||
pub reasoning_effort: Option<String>,
|
||||
pub speed: Option<String>,
|
||||
pub estimated_usage_credits_micros: i64,
|
||||
pub native_usage_usd_micros: Option<i64>,
|
||||
pub net_new_input_tokens: Option<i64>,
|
||||
pub cached_input_tokens: Option<i64>,
|
||||
pub input_tokens: Option<i64>,
|
||||
@@ -24,12 +25,13 @@ pub struct ThreadUsageBreakdownGroup {
|
||||
pub total_tokens: Option<i64>,
|
||||
}
|
||||
|
||||
/// Backend-estimated usage totals expressed in integer millionths.
|
||||
/// Backend usage totals expressed in integer millionths of their respective units.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct ThreadUsage {
|
||||
pub thread_id: String,
|
||||
pub estimated_usage_credits_micros: i64,
|
||||
pub estimated_usage_usd_micros: Option<i64>,
|
||||
pub native_usage_usd_micros: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub groups: Vec<ThreadUsageBreakdownGroup>,
|
||||
}
|
||||
@@ -50,6 +52,7 @@ struct ThreadUsageEstimate {
|
||||
thread_id: String,
|
||||
estimated_usage_credits_micros: Option<i64>,
|
||||
estimated_usage_usd_micros: Option<i64>,
|
||||
native_usage_usd_micros: Option<i64>,
|
||||
groups: Option<Vec<ThreadUsageBreakdownGroup>>,
|
||||
}
|
||||
|
||||
@@ -103,6 +106,7 @@ impl Client {
|
||||
thread_id: row.thread_id,
|
||||
estimated_usage_credits_micros: row.estimated_usage_credits_micros?,
|
||||
estimated_usage_usd_micros: row.estimated_usage_usd_micros,
|
||||
native_usage_usd_micros: row.native_usage_usd_micros,
|
||||
groups: row.groups.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,11 +41,13 @@ async fn get_thread_usage_returns_requested_thread_totals() {
|
||||
"thread_id": "thread-123",
|
||||
"estimated_usage_credits_micros": 46_000_000,
|
||||
"estimated_usage_usd_micros": 1_820_000,
|
||||
"native_usage_usd_micros": 3_250_000,
|
||||
"groups": [{
|
||||
"model": "gpt-5.4",
|
||||
"reasoning_effort": "high",
|
||||
"speed": "fast",
|
||||
"estimated_usage_credits_micros": 46_000_000,
|
||||
"native_usage_usd_micros": 3_250_000,
|
||||
"net_new_input_tokens": 80,
|
||||
"cached_input_tokens": 20,
|
||||
"input_tokens": 100,
|
||||
@@ -71,11 +73,13 @@ async fn get_thread_usage_returns_requested_thread_totals() {
|
||||
thread_id: "thread-123".to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
native_usage_usd_micros: Some(3_250_000),
|
||||
groups: vec![ThreadUsageBreakdownGroup {
|
||||
model: Some("gpt-5.4".to_string()),
|
||||
reasoning_effort: Some("high".to_string()),
|
||||
speed: Some("fast".to_string()),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
native_usage_usd_micros: Some(3_250_000),
|
||||
net_new_input_tokens: Some(80),
|
||||
cached_input_tokens: Some(20),
|
||||
input_tokens: Some(100),
|
||||
@@ -115,6 +119,7 @@ async fn get_thread_usage_accepts_credits_without_usd_estimate() {
|
||||
thread_id: "thread-123".to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
native_usage_usd_micros: None,
|
||||
groups: Vec::new(),
|
||||
}
|
||||
);
|
||||
@@ -198,3 +203,161 @@ async fn batch_usage_rejects_duplicate_and_unrequested_response_rows() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_usage_preserves_unknown_and_zero_native_group_amounts() {
|
||||
let usage: ThreadUsage = serde_json::from_value(json!({
|
||||
"thread_id": "thread-123",
|
||||
"estimated_usage_credits_micros": 0,
|
||||
"native_usage_usd_micros": null,
|
||||
"groups": [
|
||||
{ "model": "legacy", "estimated_usage_credits_micros": 0 },
|
||||
{
|
||||
"model": "unknown",
|
||||
"estimated_usage_credits_micros": 0,
|
||||
"native_usage_usd_micros": null
|
||||
},
|
||||
{
|
||||
"model": "zero",
|
||||
"estimated_usage_credits_micros": 0,
|
||||
"native_usage_usd_micros": 0
|
||||
}
|
||||
]
|
||||
}))
|
||||
.expect("decode native groups from legacy and current backend responses");
|
||||
|
||||
assert_eq!(
|
||||
usage,
|
||||
ThreadUsage {
|
||||
thread_id: "thread-123".to_string(),
|
||||
estimated_usage_credits_micros: 0,
|
||||
estimated_usage_usd_micros: None,
|
||||
native_usage_usd_micros: None,
|
||||
groups: [("legacy", None), ("unknown", None), ("zero", Some(0))]
|
||||
.into_iter()
|
||||
.map(
|
||||
|(model, native_usage_usd_micros)| ThreadUsageBreakdownGroup {
|
||||
model: Some(model.to_string()),
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
estimated_usage_credits_micros: 0,
|
||||
native_usage_usd_micros,
|
||||
net_new_input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
total_tokens: None,
|
||||
}
|
||||
)
|
||||
.collect(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_threads_usage_preserves_native_amounts_and_absence() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/usage/thread_usage/query"))
|
||||
.and(body_json(json!({ "thread_ids": ["priced", "unknown"] })))
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({
|
||||
"threads": [
|
||||
{
|
||||
"thread_id": "priced",
|
||||
"estimated_usage_credits_micros": 46_000_000,
|
||||
"estimated_usage_usd_micros": 2_505_000,
|
||||
"native_usage_usd_micros": 3_250_000,
|
||||
"groups": [
|
||||
{
|
||||
"model": "test-model-a",
|
||||
"reasoning_effort": "medium",
|
||||
"speed": "standard",
|
||||
"estimated_usage_credits_micros": 12_000_000,
|
||||
"native_usage_usd_micros": 750_000
|
||||
},
|
||||
{
|
||||
"model": "test-model-b",
|
||||
"reasoning_effort": "high",
|
||||
"speed": "fast",
|
||||
"estimated_usage_credits_micros": 34_000_000,
|
||||
"native_usage_usd_micros": 2_500_000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"thread_id": "unknown",
|
||||
"estimated_usage_credits_micros": 7_000_000,
|
||||
"groups": [{
|
||||
"model": "legacy-model",
|
||||
"estimated_usage_credits_micros": 7_000_000
|
||||
}]
|
||||
}
|
||||
]
|
||||
})))
|
||||
.expect(/*r*/ 1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new(
|
||||
server.uri(),
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
assert_eq!(
|
||||
client
|
||||
.get_threads_usage(&["priced", "unknown"])
|
||||
.await
|
||||
.expect("read native and legacy usage in one batch"),
|
||||
vec![
|
||||
ThreadUsage {
|
||||
thread_id: "priced".to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(2_505_000),
|
||||
native_usage_usd_micros: Some(3_250_000),
|
||||
groups: vec![
|
||||
ThreadUsageBreakdownGroup {
|
||||
model: Some("test-model-a".to_string()),
|
||||
reasoning_effort: Some("medium".to_string()),
|
||||
speed: Some("standard".to_string()),
|
||||
estimated_usage_credits_micros: 12_000_000,
|
||||
native_usage_usd_micros: Some(750_000),
|
||||
net_new_input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
total_tokens: None,
|
||||
},
|
||||
ThreadUsageBreakdownGroup {
|
||||
model: Some("test-model-b".to_string()),
|
||||
reasoning_effort: Some("high".to_string()),
|
||||
speed: Some("fast".to_string()),
|
||||
estimated_usage_credits_micros: 34_000_000,
|
||||
native_usage_usd_micros: Some(2_500_000),
|
||||
net_new_input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
total_tokens: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
ThreadUsage {
|
||||
thread_id: "unknown".to_string(),
|
||||
estimated_usage_credits_micros: 7_000_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
native_usage_usd_micros: None,
|
||||
groups: vec![ThreadUsageBreakdownGroup {
|
||||
model: Some("legacy-model".to_string()),
|
||||
reasoning_effort: None,
|
||||
speed: None,
|
||||
estimated_usage_credits_micros: 7_000_000,
|
||||
native_usage_usd_micros: None,
|
||||
net_new_input_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
total_tokens: None,
|
||||
}],
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user