diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 98b57033cd..db2deefdbe 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -96,6 +96,8 @@ type BillingCache interface { // ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致) type ModelPricing struct { + // UltrafastMultiplier is model-owned and independent of operator Fast pricing. + UltrafastMultiplier float64 InputPricePerToken float64 // 每token输入价格 (USD) InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD) ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken @@ -153,6 +155,9 @@ func serviceTierCostMultiplier(serviceTier string) float64 { func configuredServiceTierMultiplier(serviceTier string, pricing *ModelPricing) float64 { if pricing != nil { + if normalizeBillingServiceTier(serviceTier) == OpenAIFastTierUltrafast && pricing.UltrafastMultiplier > 0 { + return pricing.UltrafastMultiplier + } switch normalizeBillingServiceTier(serviceTier) { case "priority", "fast": if pricing.FastMultiplier != nil { @@ -1868,6 +1873,9 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing return pricing } cloned := *pricing + if isOpenAIGPT6AstraModel(normalized) { + cloned.UltrafastMultiplier = 6 + } if needsOpus55FastMultiplier { multiplier := 2.0 cloned.FastMultiplier = &multiplier diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index dba99dca4f..f2776870b5 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -2091,6 +2091,44 @@ func TestNewModelPricingAliasesRetainExplicitOverrides(t *testing.T) { } } +func TestAstraUltrafastPricingUsesSixTimesStandard(t *testing.T) { + data, err := os.ReadFile("../../resources/model-pricing/model_prices_and_context_window.json") + require.NoError(t, err) + catalog := &PricingService{} + catalog.pricingData, err = catalog.parsePricingData(data) + require.NoError(t, err) + for _, svc := range []*BillingService{newTestBillingService(), NewBillingService(&config.Config{}, &PricingService{}), NewBillingService(&config.Config{}, catalog)} { + for _, model := range []string{"gpt-6-astra", "gpt-6", "openai/gpt-6-astra"} { + for _, n := range []int{271999, 272000, 272001} { + tokens := UsageTokens{InputTokens: n - 3000, CacheReadTokens: 2000, CacheCreationTokens: 1000, OutputTokens: 500} + cost, err := svc.CalculateCostWithServiceTier(model, tokens, 1, "ultrafast") + require.NoError(t, err) + im, om := 1.0, 1.0 + if n > 272000 { + im, om = 2, 1.5 + } + require.InDelta(t, float64(n-3000)*60e-6*im, cost.InputCost, 1e-10) + require.InDelta(t, 2000*6e-6*im, cost.CacheReadCost, 1e-10) + require.InDelta(t, 1000*75e-6*im, cost.CacheCreationCost, 1e-10) + require.InDelta(t, 500*300e-6*om, cost.OutputCost, 1e-10) + } + } + } + svc := newTestBillingService() + for _, custom := range []float64{0, 1e-6} { + fast := 3.0 + p, err := svc.GetModelPricingWithChannel("gpt-6-astra", &ChannelModelPricing{InputPrice: &custom, OutputPrice: &custom, CacheWritePrice: &custom, CacheReadPrice: &custom, FastMultiplier: &fast}) + require.NoError(t, err) + require.Equal(t, 6.0, configuredServiceTierMultiplier("ultrafast", p)) + require.Equal(t, 3.0, configuredServiceTierMultiplier("priority", p)) + cost := svc.computeTokenBreakdown(p, UsageTokens{InputTokens: 1000, OutputTokens: 1000, CacheReadTokens: 1000, CacheCreationTokens: 1000}, 1, "ultrafast", false) + require.InDelta(t, custom*24000, cost.TotalCost, 1e-10) + } + p, err := svc.GetModelPricing("gpt-6-sol") + require.NoError(t, err) + require.Equal(t, 2.0, configuredServiceTierMultiplier("ultrafast", p)) +} + func TestGPT61SolExplicitZeroCacheWriteAcrossTiers(t *testing.T) { pricing := &PricingService{} var err error diff --git a/backend/internal/service/openai_codex_model_metadata.go b/backend/internal/service/openai_codex_model_metadata.go index ee085d2a89..b757908cdd 100644 --- a/backend/internal/service/openai_codex_model_metadata.go +++ b/backend/internal/service/openai_codex_model_metadata.go @@ -9,6 +9,7 @@ import ( ) var codexToolCapabilityFields = []string{ + "service_tiers", "supports_search_tool", "apply_patch_tool_type", "comp_hash", "tool_mode", "use_responses_lite", "multi_agent_reasoning_effort", "multi_agent_version", } @@ -22,7 +23,12 @@ func applyCodexToolCapabilities(dst, src map[string]json.RawMessage, overwrite b } // These Codex fields are nullable booleans or strings, never arbitrary objects. if !bytes.Equal(value, []byte("null")) { - if field == "supports_search_tool" || field == "use_responses_lite" { + if field == "service_tiers" { + var tiers []configuredCodexServiceTier + if json.Unmarshal(value, &tiers) != nil { + continue + } + } else if field == "supports_search_tool" || field == "use_responses_lite" { if !bytes.Equal(value, []byte("true")) && !bytes.Equal(value, []byte("false")) { continue } @@ -88,6 +94,17 @@ func accountCodexToolCapabilities(account *Account, modelID string) map[string]j capabilities["use_responses_lite"] = json.RawMessage("false") } } + // API Astra publicly supports Ultrafast. OAuth must advertise it in its + // account manifest; a subscription label alone does not grant the capability. + if account.IsOpenAIApiKey() && isOfficialOpenAIModelsBaseURL(baseURL) && isOpenAIGPT6AstraModel(modelID) { + tiers := configuredCodexServiceTiersForModel(modelID) + tiers = append(tiers, configuredCodexServiceTier{ID: OpenAIFastTierUltrafast, Name: "Ultrafast", Description: "Lowest latency; 6x Standard token pricing."}) + encoded, err := json.Marshal(tiers) + if err != nil { + panic(err) + } + applyCodexToolCapabilities(capabilities, map[string]json.RawMessage{"service_tiers": encoded}, false) + } return capabilities } diff --git a/backend/internal/service/openai_codex_models_service_test.go b/backend/internal/service/openai_codex_models_service_test.go index 82c4f80689..a825ba2208 100644 --- a/backend/internal/service/openai_codex_models_service_test.go +++ b/backend/internal/service/openai_codex_models_service_test.go @@ -3818,6 +3818,28 @@ func TestGPT61SolOfflineCodexCatalog(t *testing.T) { } } +func TestAstraUltrafastCatalogUsesAccountCapabilities(t *testing.T) { + for _, tc := range []struct { + accountType, base, model string + want bool + }{ + {AccountTypeAPIKey, "https://api.openai.com", "gpt-6-astra", true}, + {AccountTypeAPIKey, "https://api.openai.com", "gpt-6.1-sol", false}, + {AccountTypeAPIKey, "https://proxy.example", "gpt-6-astra", false}, + {AccountTypeOAuth, "https://chatgpt.com", "gpt-6-astra", false}, + } { + account := &Account{Platform: PlatformOpenAI, Type: tc.accountType, Credentials: map[string]any{"base_url": tc.base, "plan_type": "promax"}} + caps := accountCodexToolCapabilities(account, tc.model) + require.Equal(t, tc.want, bytes.Contains(caps["service_tiers"], []byte("ultrafast"))) + } + // Explicit native null/empty fields and account-provided tiers remain authoritative. + for _, raw := range []string{"null", "[]", `[{"id":"ultrafast","name":"Ultrafast"}]`} { + dst := map[string]json.RawMessage{"service_tiers": json.RawMessage(raw)} + require.False(t, applyCodexToolCapabilities(dst, map[string]json.RawMessage{"service_tiers": json.RawMessage(`[{"id":"priority"}]`)}, false)) + require.JSONEq(t, raw, string(dst["service_tiers"])) + } +} + func TestGPT61SolAPIKeyCatalogUsesFullResponses(t *testing.T) { account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"base_url": "https://api.openai.com"}} body, err := completeAPIKeyCodexModelsManifestMetadata([]byte(`{"models":[{"slug":"gpt-6.1-sol"}]}`), true, account) diff --git a/backend/internal/service/service_tier_billing.go b/backend/internal/service/service_tier_billing.go index 6bf74ea01e..596becc1ea 100644 --- a/backend/internal/service/service_tier_billing.go +++ b/backend/internal/service/service_tier_billing.go @@ -53,6 +53,8 @@ func serviceTierCostRank(tier string) (rank int, known bool) { return 1, true case "priority", "fast": return 2, true + case "ultrafast": + return 3, true default: return 1, false } diff --git a/backend/internal/service/service_tier_billing_test.go b/backend/internal/service/service_tier_billing_test.go index 6788d1311e..053cd9d5c7 100644 --- a/backend/internal/service/service_tier_billing_test.go +++ b/backend/internal/service/service_tier_billing_test.go @@ -16,6 +16,11 @@ func TestResolveBillingServiceTier(t *testing.T) { }{ {name: "openai priority served as default", requested: "priority", observed: "default", billing: "default", downgraded: true}, {name: "anthropic fast served as standard", requested: "fast", observed: "standard", billing: "standard", downgraded: true}, + {name: "ultrafast served as priority", requested: "ultrafast", observed: "priority", billing: "priority", downgraded: true}, + {name: "ultrafast served as default", requested: "ultrafast", observed: "default", billing: "default", downgraded: true}, + {name: "ultrafast honoured", requested: "ultrafast", observed: "ultrafast", billing: "ultrafast"}, + {name: "ultrafast missing declaration", requested: "ultrafast", billing: "ultrafast"}, + {name: "response cannot upgrade to ultrafast", requested: "priority", observed: "ultrafast", billing: "priority"}, {name: "priority honoured", requested: "priority", observed: "priority", billing: "priority"}, {name: "no declaration keeps request", requested: "priority", observed: "", billing: "priority"}, {name: "no request no declaration", requested: "", observed: "", billing: ""},