From 93a34b0f56d3046c5a6faaee3c0c402dc6b6e195 Mon Sep 17 00:00:00 2001 From: Zyy Date: Sat, 5 Sep 2026 05:27:32 +0800 Subject: [PATCH] fix(openai): add gpt-6-astra billing Add standard, Flex, Fast, long-context and cache-write rates, and classify Astra as a reasoning model for preview web-search pricing. Cover tier aliases, the context threshold and cost components with backend regression tests. Co-authored-by: Codex --- .../providers/openai/src/transport/usage.rs | 13 ++++++ .../openai/tests/transport/canonical.rs | 21 +++++++++ .../providers/openai/tests/transport/usage.rs | 46 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/backend/crates/providers/openai/src/transport/usage.rs b/backend/crates/providers/openai/src/transport/usage.rs index 2e92b4026..4c6e6a7db 100644 --- a/backend/crates/providers/openai/src/transport/usage.rs +++ b/backend/crates/providers/openai/src/transport/usage.rs @@ -202,6 +202,18 @@ struct PricingRule { // Source: https://developers.openai.com/api/docs/pricing (verified 2026-08-14). const PRICING_RULES: &[PricingRule] = &[ + // Astra: https://developers.openai.com/api/docs/models/gpt-6-astra + // Rates verified against the pricing table on 2026-09-05. + PricingRule { + model: "gpt-6-astra", + pricing: ModelPricing::new(100_000, 500_000, 10_000) + .with_cache_write(125) + .with_flex(50_000, 250_000, 5_000) + .with_fast(200_000, 1_000_000, 20_000) + .with_long(200_000, 750_000, 20_000) + .with_long_flex(100_000, 375_000, 10_000) + .with_long_fast(400_000, 1_500_000, 40_000), + }, PricingRule { model: "gpt-5.6-sol", pricing: ModelPricing::new(50_000, 300_000, 5_000) @@ -532,6 +544,7 @@ fn fixed_block_web_search_model(model: &str) -> bool { fn reasoning_model(model: &str) -> bool { let normalized = normalize_model_name(model); normalized.starts_with("gpt-5") + || model_matches_rule(&normalized, "gpt-6-astra") || normalized.starts_with("o1") || normalized.starts_with("o3") || normalized.starts_with("o4") diff --git a/backend/crates/providers/openai/tests/transport/canonical.rs b/backend/crates/providers/openai/tests/transport/canonical.rs index 340080c88..6dc5dd82d 100644 --- a/backend/crates/providers/openai/tests/transport/canonical.rs +++ b/backend/crates/providers/openai/tests/transport/canonical.rs @@ -303,6 +303,27 @@ fn decoder_should_use_non_reasoning_preview_web_search_price() { ))); } +#[test] +fn astra_decoder_should_use_reasoning_preview_web_search_price() { + let body = concat!( + "event: response.created\n", + "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_preview_search_cost\",\"model\":\"gpt-6-astra\"}}\n\n", + "event: response.completed\n", + "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_preview_search_cost\",\"model\":\"gpt-6-astra\",\"status\":\"completed\",\"output\":[{\"type\":\"web_search_call\",\"id\":\"ws_1\",\"status\":\"completed\",\"action\":{\"type\":\"search\"}}],\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"total_tokens\":0}}}\n\n", + ); + let tools = vec![json!({ "type": "web_search_preview" })]; + let events = CodexCanonicalDecoder::new("fallback") + .with_request_tool_pricing("gpt-6-astra", Some(&tools)) + .push(body.as_bytes()) + .expect("canonical preview web search response"); + + assert!(canonical_facts(&events).into_iter().any(|event| matches!( + event, + GatewayEvent::CalculatedCost(cost) + if cost.total().amount().scaled() == 100_000_000 + ))); +} + #[test] fn decoder_should_fail_closed_for_fixed_block_web_search_content() { let body = concat!( diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs index c6883ad40..4efa17e89 100644 --- a/backend/crates/providers/openai/tests/transport/usage.rs +++ b/backend/crates/providers/openai/tests/transport/usage.rs @@ -30,6 +30,52 @@ fn billing_usage( ) } +#[test] +fn astra_billing_should_preserve_components_across_tiers_and_context_boundary() { + // USD per million tokens, in order: input, cache read, cache write, output. + for (tier, input, expected, multiplier) in [ + (None, 272_000, ["10", "1", "12.5", "50"], 100), + (None, 272_001, ["20", "2", "25", "75"], 100), + (Some("flex"), 272_000, ["5", "0.5", "6.25", "25"], 50), + (Some("flex"), 272_001, ["10", "1", "12.5", "37.5"], 50), + (Some("fast"), 272_000, ["20", "2", "25", "100"], 200), + (Some("fast"), 272_001, ["40", "4", "50", "150"], 200), + (Some("priority"), 272_001, ["40", "4", "50", "150"], 200), + ] { + let breakdown = + openai_billing_breakdown("gpt-6-astra", billing_usage(input, 5, 20, 10), tier) + .expect("published Astra pricing"); + let prices = [ + breakdown.input_price_per_million(), + breakdown.cache_read_price_per_million(), + breakdown.cache_write_price_per_million(), + breakdown.output_price_per_million(), + ] + .map(|price| { + price + .amount() + .to_string() + .trim_end_matches('0') + .trim_end_matches('.') + .to_owned() + }); + assert_eq!(prices, expected, "tier={tier:?}, input={input}"); + assert_eq!(breakdown.multiplier_percent(), multiplier); + } + let breakdown = openai_billing_breakdown("gpt-6-astra", billing_usage(100, 5, 20, 10), None) + .expect("Astra component pricing"); + assert_eq!( + ( + breakdown.input_amount().amount().scaled(), + breakdown.output_amount().amount().scaled(), + breakdown.cache_read_amount().amount().scaled(), + breakdown.cache_write_amount().amount().scaled(), + breakdown.total_amount().amount().scaled(), + ), + (7_000_000, 2_500_000, 200_000, 1_250_000, 10_950_000), + ); +} + #[test] fn billing_breakdown_should_preserve_input_output_and_cache_components() { let breakdown = openai_billing_breakdown("gpt-5.6-sol", billing_usage(100, 5, 20, 10), None)