fix(pricing): add GPT-6 Sol and Luna built-in prices (#263)

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-09-25 03:51:50 +08:00
co-authored by Codex
parent ea31bf0cf7
commit 1e14250b82
4 changed files with 104 additions and 22 deletions
@@ -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")
@@ -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 =
@@ -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]
@@ -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)