From eb2eefd51e437677517add8b3965f696f46aae8c Mon Sep 17 00:00:00 2001 From: Zyy Date: Mon, 12 Oct 2026 15:46:36 +0800 Subject: [PATCH] feat(openai): add ultrafast pricing Add published GPT-6 Astra and GPT-6.1 Sol rates for short and long context. Expose Ultrafast override bands in the pricing API and editor, with billing and validation coverage. Co-authored-by: Codex --- .../gateway-admin/src/use_case/settings.rs | 10 ++- .../gateway-admin/tests/use_case/settings.rs | 45 ++++++++++ .../gateway-core/src/metering/pricing.rs | 2 + .../gateway-core/tests/metering/pricing.rs | 13 +++ .../providers/openai/src/transport/usage.rs | 32 ++++++- .../crates/providers/openai/tests/admin.rs | 29 +++++-- .../openai/tests/transport/canonical.rs | 9 +- .../providers/openai/tests/transport/usage.rs | 85 +++++++++++++++---- docs/api.md | 5 +- frontend/src/api/modules/settings/pricing.ts | 2 +- .../views/settings/pricing/PricingEditor.vue | 2 +- frontend/src/views/settings/pricing/model.ts | 2 + 12 files changed, 202 insertions(+), 34 deletions(-) diff --git a/backend/crates/gateway-admin/src/use_case/settings.rs b/backend/crates/gateway-admin/src/use_case/settings.rs index fd7ef8a58..6eb176af2 100644 --- a/backend/crates/gateway-admin/src/use_case/settings.rs +++ b/backend/crates/gateway-admin/src/use_case/settings.rs @@ -260,10 +260,12 @@ impl SettingsService for DefaultSettingsService { PricingChange::Replace(pricing) => { pricing.validate().map_err(AdminError::invalid)?; if command.provider == "xai" - && pricing - .bands - .keys() - .any(|band| matches!(band.as_str(), "image" | "flex" | "long_flex")) + && pricing.bands.keys().any(|band| { + matches!( + band.as_str(), + "image" | "flex" | "long_flex" | "ultrafast" | "long_ultrafast" + ) + }) { return Err(AdminError::invalid("xAI 不支持此价格档位")); } diff --git a/backend/crates/gateway-admin/tests/use_case/settings.rs b/backend/crates/gateway-admin/tests/use_case/settings.rs index db30fdd77..c6cd1cc8f 100644 --- a/backend/crates/gateway-admin/tests/use_case/settings.rs +++ b/backend/crates/gateway-admin/tests/use_case/settings.rs @@ -100,6 +100,51 @@ async fn client_profile_preview_rejects_unknown_provider_before_loading_settings assert_eq!(error.kind(), AdminErrorKind::Invalid); } +#[tokio::test] +async fn pricing_accepts_ultrafast_for_openai_but_rejects_it_for_xai() { + for provider in ["openai", "xai"] { + let store = Arc::new(PricingSettingsStore::default()); + let services = super::AdminHarness::new() + .settings(store.clone()) + .provider(super::test_provider(provider)) + .build() + .await; + let context = MutationContext { + actor: gateway_admin::model::MutationActor::System, + request_id: "request-ultrafast-pricing".to_owned(), + }; + for band in ["ultrafast", "long_ultrafast"] { + let pricing = serde_json::from_value(serde_json::json!({ + "multiplierBps": 10000, + "bands": { + "standard": {"input":"2", "output":"10", "cacheRead":"0.1", "cacheWrite":"2.5"}, + (band): {"input":"12", "output":"60", "cacheRead":"0.6", "cacheWrite":"15"} + } + })) + .expect("Ultrafast override"); + let update = UpdatePricing { + provider: provider.to_owned(), + models: vec!["example-model".to_owned()], + change: PricingChange::Replace(pricing), + }; + let result = services + .settings() + .update_pricing(&context, update.clone()) + .await; + if provider == "openai" { + result.expect("OpenAI accepts Ultrafast overrides"); + assert_eq!(store.updated.lock().unwrap().as_ref(), Some(&update)); + } else { + assert_eq!( + result.expect_err("xAI rejects Ultrafast").kind(), + AdminErrorKind::Invalid + ); + assert!(store.updated.lock().unwrap().is_none()); + } + } + } +} + #[tokio::test] async fn pricing_accepts_native_provider_and_rejects_unknown_sync_provider() { let store = Arc::new(PricingSettingsStore::default()); diff --git a/backend/crates/gateway-core/src/metering/pricing.rs b/backend/crates/gateway-core/src/metering/pricing.rs index 12d790159..7ca575047 100644 --- a/backend/crates/gateway-core/src/metering/pricing.rs +++ b/backend/crates/gateway-core/src/metering/pricing.rs @@ -69,9 +69,11 @@ impl ModelPriceOverride { band.as_str(), "standard" | "fast" + | "ultrafast" | "flex" | "long_standard" | "long_fast" + | "long_ultrafast" | "long_flex" | "image" ) diff --git a/backend/crates/gateway-core/tests/metering/pricing.rs b/backend/crates/gateway-core/tests/metering/pricing.rs index 16fd1e245..1983c093b 100644 --- a/backend/crates/gateway-core/tests/metering/pricing.rs +++ b/backend/crates/gateway-core/tests/metering/pricing.rs @@ -37,6 +37,19 @@ fn override_validation_rejects_unsupported_bands_and_excessive_multiplier() { assert!(price.validate().is_err()); } +#[test] +fn override_validation_accepts_ultrafast_bands() { + let pricing: ModelPriceOverride = serde_json::from_value(json!({ + "multiplierBps": 10000, + "bands": { + "ultrafast": {"input":"12", "output":"60", "cacheRead":"0.6", "cacheWrite":"15"}, + "long_ultrafast": {"input":"24", "output":"90", "cacheRead":"1.2", "cacheWrite":"30"} + } + })) + .unwrap(); + assert!(pricing.validate().is_ok()); +} + #[test] fn sync_layer_cannot_erase_manual_bands_or_multiplier() { let synced: PricingOverrides = serde_json::from_value(json!({"openai":{"model":{ diff --git a/backend/crates/providers/openai/src/transport/usage.rs b/backend/crates/providers/openai/src/transport/usage.rs index 3ce2f6418..57d4df76c 100644 --- a/backend/crates/providers/openai/src/transport/usage.rs +++ b/backend/crates/providers/openai/src/transport/usage.rs @@ -139,9 +139,11 @@ struct ModelPricing { standard: TokenRates, flex: TokenRates, fast: TokenRates, + ultrafast: TokenRates, long_standard: TokenRates, long_flex: TokenRates, long_fast: TokenRates, + long_ultrafast: TokenRates, cache_write_percent: u32, unpriced_long_context: bool, } @@ -152,9 +154,11 @@ impl ModelPricing { standard: TokenRates::new(input, output, cache_read), flex: TokenRates::ZERO, fast: TokenRates::ZERO, + ultrafast: TokenRates::ZERO, long_standard: TokenRates::ZERO, long_flex: TokenRates::ZERO, long_fast: TokenRates::ZERO, + long_ultrafast: TokenRates::ZERO, cache_write_percent: 0, unpriced_long_context: false, } @@ -170,6 +174,11 @@ impl ModelPricing { self } + const fn with_ultrafast(mut self, input: u128, output: u128, cache_read: u128) -> Self { + self.ultrafast = TokenRates::new(input, output, cache_read); + self + } + const fn with_long(mut self, input: u128, output: u128, cache_read: u128) -> Self { self.long_standard = TokenRates::new(input, output, cache_read); self @@ -185,6 +194,11 @@ impl ModelPricing { self } + const fn with_long_ultrafast(mut self, input: u128, output: u128, cache_read: u128) -> Self { + self.long_ultrafast = TokenRates::new(input, output, cache_read); + self + } + const fn with_cache_write(mut self, percent: u32) -> Self { self.cache_write_percent = percent; self @@ -206,9 +220,11 @@ impl ModelPricing { (PricingTier::Standard, false) => self.standard, (PricingTier::Flex, false) => self.flex, (PricingTier::Fast, false) => self.fast, + (PricingTier::Ultrafast, false) => self.ultrafast, (PricingTier::Standard, true) => self.long_standard, (PricingTier::Flex, true) => self.long_flex, (PricingTier::Fast, true) => self.long_fast, + (PricingTier::Ultrafast, true) => self.long_ultrafast, }; rates.is_configured().then_some(rates) } @@ -219,6 +235,7 @@ enum PricingTier { Standard, Flex, Fast, + Ultrafast, } #[derive(Clone, Copy)] @@ -230,6 +247,7 @@ struct PricingRule { // 价格来源:https://developers.openai.com/api/docs/pricing // 只登记已核验的常规价格及服务档位;临时优惠不写入内置价目 // 原有规则于 2026-09-09 核验,GPT-6.1 Sol 于 2026-09-29 核验,其余新增型号见对应条目 +// Astra 与 GPT-6.1 Sol 的 Ultrafast 档位于 2026-10-12 对照官方价目表核验 // 已按 https://developers.openai.com/api/docs/deprecations 核验至 2026-09-13, // 移除已关闭的型号;仅宣布弃用但尚未到关闭日期的型号继续保留 const PRICING_RULES: &[PricingRule] = &[ @@ -241,9 +259,11 @@ const PRICING_RULES: &[PricingRule] = &[ .with_cache_write(125) .with_flex(50_000, 250_000, 5_000) .with_fast(200_000, 1_000_000, 20_000) + .with_ultrafast(600_000, 3_000_000, 60_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), + .with_long_fast(400_000, 1_500_000, 40_000) + .with_long_ultrafast(1_200_000, 4_500_000, 120_000), }, PricingRule { model: "gpt-6.1-sol", @@ -251,9 +271,11 @@ const PRICING_RULES: &[PricingRule] = &[ .with_cache_write(125) .with_flex(10_000, 50_000, 500) .with_fast(40_000, 200_000, 2_000) + .with_ultrafast(120_000, 600_000, 6_000) .with_long(40_000, 150_000, 2_000) .with_long_flex(20_000, 75_000, 1_000) - .with_long_fast(80_000, 300_000, 4_000), + .with_long_fast(80_000, 300_000, 4_000) + .with_long_ultrafast(240_000, 900_000, 12_000), }, // https://developers.openai.com/api/docs/models/gpt-6-sol,核验日期 2026-09-24 PricingRule { @@ -608,6 +630,7 @@ fn effective_rates( PricingTier::Standard => "long_standard", PricingTier::Fast => "long_fast", PricingTier::Flex => "long_flex", + PricingTier::Ultrafast => "long_ultrafast", }; let uses_long = long_context && (default.is_some_and(|p| p.long_standard.is_configured() || p.unpriced_long_context) @@ -616,9 +639,11 @@ fn effective_rates( (PricingTier::Standard, false) => "standard", (PricingTier::Fast, false) => "fast", (PricingTier::Flex, false) => "flex", + (PricingTier::Ultrafast, false) => "ultrafast", (PricingTier::Standard, true) => "long_standard", (PricingTier::Fast, true) => "long_fast", (PricingTier::Flex, true) => "long_flex", + (PricingTier::Ultrafast, true) => "long_ultrafast", }; if let Some(rates) = custom.and_then(|p| p.bands.get(band)) { return Some(( @@ -656,9 +681,11 @@ pub(crate) fn pricing_catalog() let bands = [ ("standard", pricing.standard), ("fast", pricing.fast), + ("ultrafast", pricing.ultrafast), ("flex", pricing.flex), ("long_standard", pricing.long_standard), ("long_fast", pricing.long_fast), + ("long_ultrafast", pricing.long_ultrafast), ("long_flex", pricing.long_flex), ] .into_iter() @@ -891,6 +918,7 @@ fn pricing_tier(service_tier: Option<&str>) -> Option { None | Some("default" | "standard") => Some(PricingTier::Standard), Some("flex") => Some(PricingTier::Flex), Some("fast" | "priority") => Some(PricingTier::Fast), + Some("ultrafast") => Some(PricingTier::Ultrafast), Some(_) => None, } } diff --git a/backend/crates/providers/openai/tests/admin.rs b/backend/crates/providers/openai/tests/admin.rs index a5cacc9f9..40b206b65 100644 --- a/backend/crates/providers/openai/tests/admin.rs +++ b/backend/crates/providers/openai/tests/admin.rs @@ -412,12 +412,31 @@ async fn copying_builtin_prices_keeps_cache_read_and_write_fallback_costs() { "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 = - openai_billing_breakdown_with_override(model, usage, None, Some(&prices[model])) + let ultrafast_supported = matches!(model, "gpt-6-astra" | "gpt-6.1-sol"); + for band in ["ultrafast", "long_ultrafast"] { + assert_eq!(prices[model].bands.contains_key(band), ultrafast_supported); + } + for tier in [None, Some("ultrafast")] { + if tier.is_some() && !ultrafast_supported { + continue; + } + for input in [100, 272_001] { + let usage = OpenAiBillingUsage::new(input, 10, 20, 15); + let inherited = openai_billing_breakdown(model, usage, tier).unwrap(); + let copied = openai_billing_breakdown_with_override( + model, + usage, + tier, + Some(&prices[model]), + ) .unwrap(); - assert_eq!(inherited.total_amount(), copied.total_amount(), "{model}"); + assert_eq!( + inherited.total_amount(), + copied.total_amount(), + "{model} {tier:?} {input}" + ); + } + } } } diff --git a/backend/crates/providers/openai/tests/transport/canonical.rs b/backend/crates/providers/openai/tests/transport/canonical.rs index d05f14019..8f7b279dc 100644 --- a/backend/crates/providers/openai/tests/transport/canonical.rs +++ b/backend/crates/providers/openai/tests/transport/canonical.rs @@ -1205,6 +1205,8 @@ fn billing_should_follow_requested_tier_independently_of_response_tier() { for (requested, actual, expected) in [ (Some("priority"), Some("default"), Some(2_000_000_000)), (Some("fast"), Some("default"), Some(2_000_000_000)), + (Some("ultrafast"), Some("default"), Some(6_000_000_000)), + (Some("ultrafast"), None, Some(6_000_000_000)), (Some("auto"), Some("fast"), None), (Some("default"), Some("flex"), Some(1_000_000_000)), (Some("flex"), Some("priority"), Some(500_000_000)), @@ -1213,6 +1215,7 @@ fn billing_should_follow_requested_tier_independently_of_response_tier() { (Some("default"), Some("future"), Some(1_000_000_000)), (Some("future"), Some("default"), None), (None, Some("priority"), Some(1_000_000_000)), + (None, Some("ultrafast"), Some(1_000_000_000)), (None, None, Some(1_000_000_000)), ] { assert_eq!( @@ -1230,7 +1233,11 @@ fn billing_should_add_file_and_web_search_fees_without_tier_markup() { {"type": "file_search_call", "id": "fs_2", "status": "completed"}, {"type": "web_search_call", "id": "ws_1", "status": "completed", "action": {"type": "search"}} ]); - for (tier, expected) in [("default", 1_150_000_000), ("fast", 2_150_000_000)] { + for (tier, expected) in [ + ("default", 1_150_000_000), + ("fast", 2_150_000_000), + ("ultrafast", 6_150_000_000), + ] { assert_eq!( pricing_response_cost( Some(tier), diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs index c8d4c3193..5b8b835c1 100644 --- a/backend/crates/providers/openai/tests/transport/usage.rs +++ b/backend/crates/providers/openai/tests/transport/usage.rs @@ -136,6 +136,48 @@ fn new_gpt_6_models_should_use_published_prices_across_tiers_and_context_boundar } } +#[test] +fn ultrafast_billing_uses_published_prices_across_the_context_boundary() { + // 每档依次核对每百万 Token 的输入、缓存读取、缓存写入和输出价格 + for (model, input, expected, total) in [ + ("gpt-6-astra", 272_000, ["60", "6", "75", "300"], "16.32057"), + ( + "gpt-6-astra", + 272_001, + ["120", "12", "150", "450"], + "32.64051", + ), + ( + "gpt-6.1-sol", + 272_000, + ["12", "0.6", "15", "60"], + "3.264102", + ), + ( + "gpt-6.1-sol", + 272_001, + ["24", "1.2", "30", "90"], + "6.528078", + ), + ] { + let breakdown = + openai_billing_breakdown(model, billing_usage(input, 5, 20, 10), Some("ultrafast")) + .expect("published Ultrafast 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().canonical()); + assert_eq!(prices, expected, "{model} {input}"); + assert_eq!(breakdown.total_amount().amount().canonical(), total); + assert_eq!(breakdown.service_tier(), Some("ultrafast")); + assert_eq!(breakdown.multiplier_percent(), 600); + assert_eq!(breakdown.long_context_billing_applied(), input > 272_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) @@ -405,7 +447,12 @@ fn billing_should_not_inherit_prices_for_unknown_models_or_tiers() { ("gpt-5.6-cyber", 1, Some("fast")), ("gpt-5.3-codex", 1, Some("flex")), ("gpt-6-astra", 1, Some("auto")), - ("gpt-6-astra", 1, Some("ultrafast")), + ("gpt-6-astra", 1, Some("future")), + ("gpt-6-sol", 1, Some("ultrafast")), + ("gpt-6-sol", 272_001, Some("ultrafast")), + ("gpt-6-luna", 1, Some("ultrafast")), + ("gpt-6-luna", 272_001, Some("ultrafast")), + ("gpt-5.4", 1, Some("ultrafast")), ] { assert!( openai_billing_breakdown(model, billing_usage(input, 1, 0, 0), tier).is_none(), @@ -906,29 +953,31 @@ fn custom_price_has_exact_cache_and_multiplier_math() { } #[test] -fn a_custom_long_priority_band_does_not_require_a_long_standard_band() { - let pricing = serde_json::from_value(json!({"multiplierBps":10000,"bands":{ - "standard":{"input":"2","output":"10","cacheRead":"0","cacheWrite":"0"}, - "long_fast":{"input":"8","output":"40","cacheRead":"0","cacheWrite":"0"} - }})) - .unwrap(); - let result = openai_billing_breakdown_with_override( - "custom-model", - billing_usage(300_000, 0, 0, 0), - Some("priority"), - Some(&pricing), - ) - .unwrap(); - assert_eq!(result.total_amount().amount().canonical(), "2.4"); - assert_eq!(result.standard_amount().amount().canonical(), "0.6"); - assert!(result.long_context_billing_applied()); +fn a_custom_long_tier_does_not_require_a_long_standard_band() { + for (tier, band) in [("priority", "long_fast"), ("ultrafast", "long_ultrafast")] { + let pricing = serde_json::from_value(json!({"multiplierBps":10000,"bands":{ + "standard":{"input":"2","output":"10","cacheRead":"0","cacheWrite":"0"}, + (band):{"input":"8","output":"40","cacheRead":"0","cacheWrite":"0"} + }})) + .unwrap(); + let result = openai_billing_breakdown_with_override( + "custom-model", + billing_usage(300_000, 0, 0, 0), + Some(tier), + Some(&pricing), + ) + .unwrap(); + assert_eq!(result.total_amount().amount().canonical(), "2.4"); + assert_eq!(result.standard_amount().amount().canonical(), "0.6"); + assert!(result.long_context_billing_applied()); + } } #[test] fn multiplier_only_preserves_builtin_tiers_and_unknown_models_remain_unknown() { let custom: ModelPriceOverride = serde_json::from_value(json!({"multiplierBps":20000,"bands":{}})).unwrap(); - for tier in [None, Some("priority"), Some("flex")] { + for tier in [None, Some("priority"), Some("flex"), Some("ultrafast")] { for input in [100, 272_001] { let usage = billing_usage(input, 10, 20, 5); let original = openai_billing_breakdown("gpt-6-astra", usage, tier).unwrap(); diff --git a/docs/api.md b/docs/api.md index 370e789c3..527a53905 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1536,9 +1536,10 @@ Guardian 以 `subagent_kind` 或 `client_metadata.x-openai-subagent` 值 `guardi 每个档位包含四个非负十进制字符串:`input`、`output`、`cacheRead`、`cacheWrite`,单位 USD / 百万 Token,范围 0~1000000、最多四位小数。`"0"` 表示免费,缺少整个档位表示继承;不能只缺少部分单价 -`bands` 可用键为 `standard`、`fast`、`flex`、`long_standard`、`long_fast`、`long_flex`、`image`。 +`bands` 可用键为 `standard`、`fast`、`ultrafast`、`flex`、`long_standard`、`long_fast`、`long_ultrafast`、`long_flex`、`image`。 OpenAI 长上下文为输入超过 272000 Token,xAI 为输入达到 200000 Token;仍按 Provider 支持的 -模型与服务档位判断是否能估算。xAI 不接受 `flex`、`long_flex`、`image`。图像端点的 `standard` +模型与服务档位判断是否能估算。OpenAI 内置 Ultrafast 价目覆盖 `gpt-6-astra` 与 `gpt-6.1-sol`。 +xAI 不接受 `flex`、`long_flex`、`ultrafast`、`long_ultrafast`、`image`。图像端点的 `standard` 用于文本输入,`image` 用于图像 Token;不能用文本输出单价替代图像输出单价。用量无法完整拆分时不估算 `multiplierBps` 为 0~1000000 的整数,10000 表示 1 倍、0 表示本地估算为零,最大 100 倍。 diff --git a/frontend/src/api/modules/settings/pricing.ts b/frontend/src/api/modules/settings/pricing.ts index 08959cfa6..45c46ba48 100644 --- a/frontend/src/api/modules/settings/pricing.ts +++ b/frontend/src/api/modules/settings/pricing.ts @@ -1,7 +1,7 @@ import type { RequestOptions } from '../../request' import request from '../../request' -export type PriceBand = 'standard' | 'fast' | 'flex' | 'long_standard' | 'long_fast' | 'long_flex' | 'image' +export type PriceBand = 'standard' | 'fast' | 'ultrafast' | 'flex' | 'long_standard' | 'long_fast' | 'long_ultrafast' | 'long_flex' | 'image' export interface TokenPrices { input: string diff --git a/frontend/src/views/settings/pricing/PricingEditor.vue b/frontend/src/views/settings/pricing/PricingEditor.vue index 8058d5e63..f2a083020 100644 --- a/frontend/src/views/settings/pricing/PricingEditor.vue +++ b/frontend/src/views/settings/pricing/PricingEditor.vue @@ -32,7 +32,7 @@ const priceMode = computed({ const bandOptions = computed(() => bands .filter(band => props.row?.base.bands.image ? ['standard', 'image'].includes(band.value) - : props.provider !== 'xai' || !['flex', 'long_flex', 'image'].includes(band.value)) + : props.provider !== 'xai' || !['flex', 'long_flex', 'ultrafast', 'long_ultrafast', 'image'].includes(band.value)) .map(band => ({ ...band, label: band.value === 'standard' && props.row?.base.bands.image ? '文本 Token' : band.label }))) watch(open, (value) => { diff --git a/frontend/src/views/settings/pricing/model.ts b/frontend/src/views/settings/pricing/model.ts index e4ee448ca..19b74c9f1 100644 --- a/frontend/src/views/settings/pricing/model.ts +++ b/frontend/src/views/settings/pricing/model.ts @@ -3,9 +3,11 @@ import type { ModelPricing, PriceBand, PricingCatalog, TokenPrices } from '@/api export const bands: { value: PriceBand, label: string }[] = [ { value: 'standard', label: '标准' }, { value: 'fast', label: 'Priority / Fast' }, + { value: 'ultrafast', label: 'Ultrafast' }, { value: 'flex', label: 'Flex' }, { value: 'long_standard', label: '长上下文 · 标准' }, { value: 'long_fast', label: '长上下文 · Priority' }, + { value: 'long_ultrafast', label: '长上下文 · Ultrafast' }, { value: 'long_flex', label: '长上下文 · Flex' }, { value: 'image', label: '图像 Token' }, ]