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 <noreply@openai.com>
This commit is contained in:
Zyy
2026-09-05 05:27:32 +08:00
co-authored by Codex
parent aa3a07209a
commit 93a34b0f56
3 changed files with 80 additions and 0 deletions
@@ -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")
@@ -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!(
@@ -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)