From 1e14250b820fcd5bd0fad13a223c9d18e2a4177e Mon Sep 17 00:00:00 2001 From: Zyy Date: Fri, 25 Sep 2026 03:51:50 +0800 Subject: [PATCH] fix(pricing): add GPT-6 Sol and Luna built-in prices (#263) Co-authored-by: Codex --- .../providers/openai/src/transport/usage.rs | 30 ++++++++++-- .../crates/providers/openai/tests/admin.rs | 2 +- .../openai/tests/transport/canonical.rs | 47 ++++++++++++------- .../providers/openai/tests/transport/usage.rs | 47 +++++++++++++++++++ 4 files changed, 104 insertions(+), 22 deletions(-) diff --git a/backend/crates/providers/openai/src/transport/usage.rs b/backend/crates/providers/openai/src/transport/usage.rs index 3dbaeac69..2790a3861 100644 --- a/backend/crates/providers/openai/src/transport/usage.rs +++ b/backend/crates/providers/openai/src/transport/usage.rs @@ -226,9 +226,9 @@ struct PricingRule { pricing: ModelPricing, } -// 价格来源:https://developers.openai.com/api/docs/pricing,核验日期 2026-09-09。 -// 使用常规价,不采用 Sol 的临时 $4/$20 优惠;缓存、Flex、Fast 和长上下文 -// 档位也统一按常规价计算。 +// 价格来源:https://developers.openai.com/api/docs/pricing。 +// 只登记已核验的常规价格及服务档位;临时优惠不写入内置价目。 +// 原有规则于 2026-09-09 核验,新增型号的核验日期见对应条目。 // 已按 https://developers.openai.com/api/docs/deprecations 核验至 2026-09-13, // 移除已关闭的型号;仅宣布弃用但尚未到关闭日期的型号继续保留。 const PRICING_RULES: &[PricingRule] = &[ @@ -244,6 +244,28 @@ const PRICING_RULES: &[PricingRule] = &[ .with_long_flex(100_000, 375_000, 10_000) .with_long_fast(400_000, 1_500_000, 40_000), }, + // https://developers.openai.com/api/docs/models/gpt-6-sol,核验日期 2026-09-24。 + PricingRule { + model: "gpt-6-sol", + pricing: ModelPricing::new(20_000, 100_000, 2_000) + .with_cache_write(125) + .with_flex(10_000, 50_000, 1_000) + .with_fast(40_000, 200_000, 4_000) + .with_long(40_000, 150_000, 4_000) + .with_long_flex(20_000, 75_000, 2_000) + .with_long_fast(80_000, 300_000, 8_000), + }, + // https://developers.openai.com/api/docs/models/gpt-6-luna,核验日期 2026-09-24。 + PricingRule { + model: "gpt-6-luna", + pricing: ModelPricing::new(1_000, 5_000, 100) + .with_cache_write(125) + .with_flex(500, 2_500, 50) + .with_fast(2_000, 10_000, 200) + .with_long(2_000, 7_500, 200) + .with_long_flex(1_000, 3_750, 100) + .with_long_fast(4_000, 15_000, 400), + }, PricingRule { model: "gpt-5.6-sol", pricing: ModelPricing::new(50_000, 300_000, 5_000) @@ -842,7 +864,7 @@ fn reasoning_model(model: &str) -> bool { let normalized = normalize_model_name(model); let model = pricing_model_name(&normalized); model.starts_with("gpt-5") - || model == "gpt-6-astra" + || matches!(model, "gpt-6-astra" | "gpt-6-sol" | "gpt-6-luna") || model.starts_with("o1") || model.starts_with("o3") || model.starts_with("o4") diff --git a/backend/crates/providers/openai/tests/admin.rs b/backend/crates/providers/openai/tests/admin.rs index 63b12c994..5e03ffc0d 100644 --- a/backend/crates/providers/openai/tests/admin.rs +++ b/backend/crates/providers/openai/tests/admin.rs @@ -323,7 +323,7 @@ async fn copying_builtin_prices_keeps_cache_read_and_write_fallback_costs() { .await .unwrap(); let prices = bundle.admin_provider().pricing_catalog(); - for model in ["gpt-4", "gpt-4o", "gpt-6-astra"] { + for model in ["gpt-4", "gpt-4o", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna"] { let usage = OpenAiBillingUsage::new(100, 10, 20, 15); let inherited = openai_billing_breakdown(model, usage, None).unwrap(); let copied = diff --git a/backend/crates/providers/openai/tests/transport/canonical.rs b/backend/crates/providers/openai/tests/transport/canonical.rs index 8b0c4c033..539f07e5c 100644 --- a/backend/crates/providers/openai/tests/transport/canonical.rs +++ b/backend/crates/providers/openai/tests/transport/canonical.rs @@ -256,7 +256,8 @@ fn decoder_should_bill_the_sent_model_independently_of_the_response_model() { ("gpt-5.6-sol", Some("gpt-6-astra"), Some(6_875_000)), ("gpt-5.6-sol", Some("gpt-5.6-sol"), Some(6_875_000)), ("gpt-5.6-sol", None, Some(6_875_000)), - ("gpt-6-sol", Some("gpt-5.6-sol"), None), + ("gpt-6-sol", Some("gpt-5.6-sol"), Some(2_550_000)), + ("gpt-6-luna", Some("gpt-6-astra"), Some(127_500)), ] { let created = json!({"type":"response.created","response":{"id":"resp_model_cost","model":returned}}); @@ -396,24 +397,36 @@ 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", - ); +fn gpt_6_decoders_should_use_reasoning_preview_web_search_price() { let tools = vec![json!({ "type": "web_search_preview" })]; - let events = CodexCanonicalDecoder::new("gpt-6-astra") - .with_request_tool_pricing("gpt-6-astra", Some(&tools)) - .push(body.as_bytes()) - .expect("canonical preview web search response"); + for model in ["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"] { + let created = json!({ + "type":"response.created", + "response":{"id":"resp_preview_search_cost","model":model} + }); + let completed = json!({ + "type":"response.completed", + "response":{ + "id":"resp_preview_search_cost","model":model,"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} + } + }); + let body = format!("data: {created}\n\ndata: {completed}\n\n"); + let events = CodexCanonicalDecoder::new(model) + .with_request_tool_pricing(model, 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 - ))); + assert!( + canonical_facts(&events).into_iter().any(|event| matches!( + event, + GatewayEvent::CalculatedCost(cost) + if cost.total().amount().scaled() == 100_000_000 + )), + "{model}" + ); + } } #[test] diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs index f19d597f4..5c2842e6f 100644 --- a/backend/crates/providers/openai/tests/transport/usage.rs +++ b/backend/crates/providers/openai/tests/transport/usage.rs @@ -76,6 +76,53 @@ fn astra_billing_should_preserve_components_across_tiers_and_context_boundary() ); } +#[test] +fn new_gpt_6_models_should_use_published_prices_across_tiers_and_context_boundary() { + // 每档依次核对每百万 Token 的输入、缓存读取、缓存写入和输出价格。 + for (model, cases) in [ + ( + "gpt-6-sol", + [ + (None, 272_000, ["2", "0.2", "2.5", "10"]), + (None, 272_001, ["4", "0.4", "5", "15"]), + (Some("flex"), 272_000, ["1", "0.1", "1.25", "5"]), + (Some("flex"), 272_001, ["2", "0.2", "2.5", "7.5"]), + (Some("fast"), 272_000, ["4", "0.4", "5", "20"]), + (Some("priority"), 272_001, ["8", "0.8", "10", "30"]), + ], + ), + ( + "gpt-6-luna", + [ + (None, 272_000, ["0.1", "0.01", "0.125", "0.5"]), + (None, 272_001, ["0.2", "0.02", "0.25", "0.75"]), + (Some("flex"), 272_000, ["0.05", "0.005", "0.0625", "0.25"]), + (Some("flex"), 272_001, ["0.1", "0.01", "0.125", "0.375"]), + (Some("fast"), 272_000, ["0.2", "0.02", "0.25", "1"]), + (Some("priority"), 272_001, ["0.4", "0.04", "0.5", "1.5"]), + ], + ), + ] { + for (tier, input, expected) in cases { + let breakdown = openai_billing_breakdown(model, billing_usage(input, 5, 20, 10), tier) + .expect("published GPT-6 pricing"); + let actual = [ + 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().canonical()); + assert_eq!(actual, expected, "{model} {tier:?} {input}"); + assert_eq!( + breakdown.long_context_billing_applied(), + input > 272_000, + "{model} {tier:?} {input}" + ); + } + } +} + #[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)