From 91920c263ec195248d38414c235af649da29f859 Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Sun, 16 Aug 2026 13:54:42 -0700 Subject: [PATCH 1/4] fix(codec): accept scalar OpenRouter usage cost Signed-off-by: Bryan Bednarski --- crates/core/src/codec/openai_chat.rs | 19 ++++++++++-- .../tests/unit/codec/openai_chat_tests.rs | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/core/src/codec/openai_chat.rs b/crates/core/src/codec/openai_chat.rs index dc3c46dca..a98deaf27 100644 --- a/crates/core/src/codec/openai_chat.rs +++ b/crates/core/src/codec/openai_chat.rs @@ -93,7 +93,14 @@ struct RawChatUsage { prompt_tokens_details: Option, #[serde(rename = "cost_usd")] provider_cost: Option, - cost: Option, + cost: Option, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum RawChatUsageCost { + Scalar(f64), + Detailed(RawUsageCost), } #[derive(Deserialize)] @@ -1167,13 +1174,21 @@ impl LlmResponseCodec for OpenAIChatCodec { let model_for_pricing = raw.model.as_deref(); let model_provider = infer_model_provider("openai", model_for_pricing); let usage = raw.usage.map(|u| { + let (scalar_provider_cost, detailed_cost) = match u.cost { + Some(RawChatUsageCost::Scalar(cost)) => (Some(cost), None), + Some(RawChatUsageCost::Detailed(cost)) => (None, Some(cost)), + None => (None, None), + }; let mut usage = Usage { prompt_tokens: u.prompt_tokens, completion_tokens: u.completion_tokens, total_tokens: u.total_tokens, cache_read_tokens: u.prompt_tokens_details.and_then(|d| d.cached_tokens), cache_write_tokens: None, - cost: provider_reported_cost(u.provider_cost, u.cost), + cost: provider_reported_cost( + u.provider_cost.or(scalar_provider_cost), + detailed_cost, + ), }; if usage.cost.is_none() { usage.cost = model_for_pricing.and_then(|model| { diff --git a/crates/core/tests/unit/codec/openai_chat_tests.rs b/crates/core/tests/unit/codec/openai_chat_tests.rs index cb3fdb3b8..9841b6536 100644 --- a/crates/core/tests/unit/codec/openai_chat_tests.rs +++ b/crates/core/tests/unit/codec/openai_chat_tests.rs @@ -175,6 +175,35 @@ fn test_decode_response_provider_reported_cost() { assert_eq!(cost.source, CostSource::ProviderReported); } +#[test] +fn test_decode_response_openrouter_scalar_provider_reported_cost() { + let codec = OpenAIChatCodec; + let response = json!({ + "id": "gen-openrouter-cost", + "object": "chat.completion", + "model": "nvidia/nemotron-3-ultra-550b-a55b:free", + "provider": "Nvidia", + "choices": [{ + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cost": 0, + "cost_details": {"upstream_inference_cost": 0} + } + }); + + let resp = codec.decode_response(&response).unwrap(); + let cost = resp.usage.unwrap().cost.unwrap(); + + assert_eq!(cost.total, Some(0.0)); + assert_eq!(cost.currency, "USD"); + assert_eq!(cost.source, CostSource::ProviderReported); +} + #[test] fn test_decode_response_finish_reason_stop() { let codec = OpenAIChatCodec; From e60d9d623d470d42b390d2dce2c7234815af680e Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Sun, 16 Aug 2026 14:19:50 -0700 Subject: [PATCH 2/4] test(codec): cover scalar cost precedence Signed-off-by: Bryan Bednarski --- .../tests/unit/codec/openai_chat_tests.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/core/tests/unit/codec/openai_chat_tests.rs b/crates/core/tests/unit/codec/openai_chat_tests.rs index 9841b6536..56d481f5f 100644 --- a/crates/core/tests/unit/codec/openai_chat_tests.rs +++ b/crates/core/tests/unit/codec/openai_chat_tests.rs @@ -204,6 +204,34 @@ fn test_decode_response_openrouter_scalar_provider_reported_cost() { assert_eq!(cost.source, CostSource::ProviderReported); } +#[test] +fn test_decode_response_scalar_provider_reported_cost_honors_cost_usd_precedence() { + let codec = OpenAIChatCodec; + let scalar_response = json!({"usage": {"cost": 0.0123}}); + let scalar_cost = codec + .decode_response(&scalar_response) + .unwrap() + .usage + .unwrap() + .cost + .unwrap(); + + assert_eq!(scalar_cost.total, Some(0.0123)); + assert_eq!(scalar_cost.source, CostSource::ProviderReported); + + let conflicting_response = json!({"usage": {"cost_usd": 0.0456, "cost": 0.0123}}); + let conflicting_cost = codec + .decode_response(&conflicting_response) + .unwrap() + .usage + .unwrap() + .cost + .unwrap(); + + assert_eq!(conflicting_cost.total, Some(0.0456)); + assert_eq!(conflicting_cost.source, CostSource::ProviderReported); +} + #[test] fn test_decode_response_finish_reason_stop() { let codec = OpenAIChatCodec; From 8a7929e24d0636b28ec3543ee5322589d26f464f Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Mon, 17 Aug 2026 08:26:29 -0700 Subject: [PATCH 3/4] docs(codec): document scalar OpenRouter costs Signed-off-by: Bryan Bednarski --- docs/integrate-into-frameworks/provider-response-codecs.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrate-into-frameworks/provider-response-codecs.mdx b/docs/integrate-into-frameworks/provider-response-codecs.mdx index 19c1c65a5..d00b5eb0e 100644 --- a/docs/integrate-into-frameworks/provider-response-codecs.mdx +++ b/docs/integrate-into-frameworks/provider-response-codecs.mdx @@ -368,6 +368,7 @@ Built-in codecs normalize provider field names as follows: | `total_tokens` | `total_tokens` | `total_tokens` | computed | `totalTokens` | `totalTokenCount` (or computed as prompt + candidates + thinking) | | `cache_read_tokens` | `prompt_tokens_details.cached_tokens` | `input_tokens_details.cached_tokens` | `cache_read_input_tokens` | `promptTokensDetails.cachedTokens` | `cachedContentTokenCount` | | `cache_write_tokens` | — | — | `cache_creation_input_tokens` | — | — | +| `cost` | `cost_usd`, structured `cost`, or scalar `cost` (OpenRouter; USD provider-reported total) | `cost_usd` or structured `cost` | `cost_usd` or structured `cost` | — | — | Gemini `generateContent` thinking tokens (`thoughtsTokenCount`) are stored in `api_specific.thoughts_tokens` rather than `completion_tokens`, because Google bills them as output tokens but reports them separately. The cost estimate folds them into the effective output-token count so that the pricing table reflects the real billing cost. From 7aca4ea0b024d85451625c217d173347900f6a60 Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Mon, 17 Aug 2026 08:32:21 -0700 Subject: [PATCH 4/4] test(codec): cover detailed cost precedence Signed-off-by: Bryan Bednarski --- .../tests/unit/codec/openai_chat_tests.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/core/tests/unit/codec/openai_chat_tests.rs b/crates/core/tests/unit/codec/openai_chat_tests.rs index 56d481f5f..c5fe8527d 100644 --- a/crates/core/tests/unit/codec/openai_chat_tests.rs +++ b/crates/core/tests/unit/codec/openai_chat_tests.rs @@ -230,6 +230,30 @@ fn test_decode_response_scalar_provider_reported_cost_honors_cost_usd_precedence assert_eq!(conflicting_cost.total, Some(0.0456)); assert_eq!(conflicting_cost.source, CostSource::ProviderReported); + + let conflicting_detailed_response = json!({ + "usage": { + "cost_usd": 0.0456, + "cost": { + "total": 0.0123, + "input": 0.004, + "output": 0.0083 + } + } + }); + let conflicting_detailed_cost = codec + .decode_response(&conflicting_detailed_response) + .unwrap() + .usage + .unwrap() + .cost + .unwrap(); + + assert_eq!(conflicting_detailed_cost.total, Some(0.0456)); + assert_eq!( + conflicting_detailed_cost.source, + CostSource::ProviderReported + ); } #[test]