mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(pricing): add GPT-6 Sol and Luna built-in prices (#263)
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user