diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index e2046dd979..a781936598 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -276,8 +276,9 @@ func (s *BillingService) initFallbackPricing() { LongContextInputMultiplier: openAIGPT54LongContextInputMultiplier, LongContextOutputMultiplier: openAIGPT54LongContextOutputMultiplier, } - // GPT-5.5 暂无独立定价,回退到 GPT-5.4 + // GPT-5.5 / GPT-5.5 Pro 暂无独立定价,回退到 GPT-5.4。 s.fallbackPrices["gpt-5.5"] = s.fallbackPrices["gpt-5.4"] + s.fallbackPrices["gpt-5.5-pro"] = s.fallbackPrices["gpt-5.4"] s.fallbackPrices["gpt-5.4-mini"] = &ModelPricing{ InputPricePerToken: 7.5e-7, @@ -666,6 +667,8 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { // OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。 if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" { switch normalized { + case "gpt-5.5-pro": + return s.fallbackPrices["gpt-5.5-pro"] case "gpt-5.5": return s.fallbackPrices["gpt-5.5"] case "gpt-5.4-mini": @@ -1057,7 +1060,7 @@ func isOpenAIGPT54Model(model string) bool { // normalizeCodexModel 的默认兜底把非 OpenAI 模型(claude-*、gemini-*、gpt-4o) // 误识别为 gpt-5.4。 normalized := normalizeKnownOpenAICodexModel(model) - return normalized == "gpt-5.4" || normalized == "gpt-5.5" + return normalized == "gpt-5.4" || normalized == "gpt-5.5" || normalized == "gpt-5.5-pro" } // CalculateCostWithConfig 使用配置中的默认倍率计算费用 diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 62792dc5df..92c143c6ff 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -263,6 +263,25 @@ func TestCalculateCost_OpenAIGPT54LongContextAppliesWholeSessionMultipliers(t *t require.InDelta(t, expectedInput+expectedOutput, cost.ActualCost, 1e-10) } +func TestCalculateCost_OpenAIGPT55ProUsesGPT55PricingPolicy(t *testing.T) { + svc := newTestBillingService() + + tokens := UsageTokens{ + InputTokens: 300000, + OutputTokens: 4000, + } + + cost, err := svc.CalculateCost("gpt-5.5-pro", tokens, 1.0) + require.NoError(t, err) + + expectedInput := float64(tokens.InputTokens) * 2.5e-6 * 2.0 + expectedOutput := float64(tokens.OutputTokens) * 15e-6 * 1.5 + require.InDelta(t, expectedInput, cost.InputCost, 1e-10) + require.InDelta(t, expectedOutput, cost.OutputCost, 1e-10) + require.InDelta(t, expectedInput+expectedOutput, cost.TotalCost, 1e-10) + require.InDelta(t, expectedInput+expectedOutput, cost.ActualCost, 1e-10) +} + // 回归测试 #2293:长上下文计费触发时,cache_read_tokens 也应应用 LongContextInputMultiplier。 // 修复前:CacheReadCost = tokens * 0.25e-6 (漏乘倍率,少计费用)。 // 修复后:CacheReadCost = tokens * 0.25e-6 * LongContextInputMultiplier(=2.0)。 diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index 4bb29167f9..c551ba54f1 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -10,6 +10,7 @@ import ( var codexModelMap = map[string]string{ "gpt-5.5": "gpt-5.5", + "gpt-5.5-pro": "gpt-5.5-pro", "codex-auto-review": "codex-auto-review", "gpt-5.4": "gpt-5.4", "gpt-5.4-mini": "gpt-5.4-mini", @@ -61,6 +62,7 @@ var codexVersionModelPrefixes = []struct { {prefix: "gpt-5.3-codex", target: "gpt-5.3-codex"}, {prefix: "gpt-5.4-mini", target: "gpt-5.4-mini"}, {prefix: "gpt-5.4-nano", target: "gpt-5.4-nano"}, + {prefix: "gpt-5.5-pro", target: "gpt-5.5-pro"}, {prefix: "gpt-5.5", target: "gpt-5.5"}, {prefix: "gpt-5.4", target: "gpt-5.4"}, {prefix: "gpt-5.2", target: "gpt-5.2"}, diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index d3bdc0a52e..bb27c81352 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -898,6 +898,10 @@ func TestNormalizeCodexModel_Gpt53(t *testing.T) { "gpt-5.4": "gpt-5.4", "gpt5.5": "gpt-5.5", "openai/gpt5.5": "gpt-5.5", + "gpt-5.5-pro": "gpt-5.5-pro", + "gpt5.5-pro": "gpt-5.5-pro", + "openai/gpt5.5-pro": "gpt-5.5-pro", + "gpt-5.5-pro-high": "gpt-5.5-pro", "codex-auto-review": "codex-auto-review", "gpt5.4": "gpt-5.4", "gpt-5.4-high": "gpt-5.4", diff --git a/backend/internal/service/openai_compat_prompt_cache_key_test.go b/backend/internal/service/openai_compat_prompt_cache_key_test.go index 3fe7db6ef6..ce1c68a2b7 100644 --- a/backend/internal/service/openai_compat_prompt_cache_key_test.go +++ b/backend/internal/service/openai_compat_prompt_cache_key_test.go @@ -16,6 +16,7 @@ func mustRawJSON(t *testing.T, s string) json.RawMessage { func TestShouldAutoInjectPromptCacheKeyForCompat(t *testing.T) { require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.5")) + require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.5-pro")) require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.4")) require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.4-mini")) require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.2")) diff --git a/backend/internal/service/openai_model_alias.go b/backend/internal/service/openai_model_alias.go index 2fa2c90efd..ac2a8cf942 100644 --- a/backend/internal/service/openai_model_alias.go +++ b/backend/internal/service/openai_model_alias.go @@ -65,6 +65,8 @@ func normalizeKnownOpenAICodexModel(model string) string { } switch { + case strings.Contains(normalized, "gpt-5.5-pro"): + return "gpt-5.5-pro" case strings.Contains(normalized, "gpt-5.5"): return "gpt-5.5" case strings.Contains(normalized, "gpt-5.4-mini"): diff --git a/backend/internal/service/openai_model_mapping_test.go b/backend/internal/service/openai_model_mapping_test.go index 020e887528..0f65740be7 100644 --- a/backend/internal/service/openai_model_mapping_test.go +++ b/backend/internal/service/openai_model_mapping_test.go @@ -94,6 +94,15 @@ func TestResolveOpenAIForwardModel(t *testing.T) { defaultMappedModel: "gpt-5.4", expectedModel: "gpt-5.5", }, + { + name: "preserves gpt-5.5-pro instead of group default", + account: &Account{ + Credentials: map[string]any{}, + }, + requestedModel: "gpt-5.5-pro", + defaultMappedModel: "gpt-5.5", + expectedModel: "gpt-5.5-pro", + }, { name: "preserves compact-spelled gpt5.5 instead of group default", account: &Account{ @@ -261,6 +270,12 @@ func TestNormalizeOpenAIModelForUpstream(t *testing.T) { model: "gpt-5.4-high", want: "gpt-5.4", }, + { + name: "oauth preserves GPT-5.5 Pro model", + account: &Account{Type: AccountTypeOAuth}, + model: "openai/gpt-5.5-pro", + want: "gpt-5.5-pro", + }, { name: "oauth preserves codex auto review model", account: &Account{Type: AccountTypeOAuth}, @@ -303,3 +318,17 @@ func TestUsageBillingModelCandidatesPreserveCodexAutoReviewModel(t *testing.T) { } } } + +func TestUsageBillingModelCandidatesPreserveGPT55ProModel(t *testing.T) { + candidates := usageBillingModelCandidates("openai/gpt-5.5-pro") + + expected := []string{"openai/gpt-5.5-pro", "gpt-5.5-pro"} + if len(candidates) != len(expected) { + t.Fatalf("usageBillingModelCandidates(openai/gpt-5.5-pro) = %#v, want %#v", candidates, expected) + } + for i := range expected { + if candidates[i] != expected[i] { + t.Fatalf("usageBillingModelCandidates(openai/gpt-5.5-pro) = %#v, want %#v", candidates, expected) + } + } +}