From 3c8be00131e69d432246855d14ff014d9f7c10e2 Mon Sep 17 00:00:00 2001 From: lyy0709 <65712338+lyy0709@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:04:44 +0800 Subject: [PATCH] feat(openai): add GPT-6 Astra support --- backend/internal/pkg/openai/constants.go | 2 + backend/internal/pkg/openai/constants_test.go | 5 ++ backend/internal/service/billing_service.go | 18 +++- .../service/openai_codex_models_service.go | 16 +++- .../openai_codex_models_service_test.go | 29 ++++-- .../service/openai_codex_transform.go | 1 + .../service/openai_codex_transform_test.go | 4 + .../service/openai_gateway_request_body.go | 2 +- .../internal/service/openai_gpt56_max_test.go | 3 +- .../internal/service/openai_model_alias.go | 7 ++ .../service/openai_model_alias_test.go | 6 ++ backend/internal/service/pricing_service.go | 26 ++++++ .../internal/service/pricing_service_test.go | 89 +++++++++++++++++++ frontend/src/components/keys/UseKeyModal.vue | 34 +++++++ .../keys/__tests__/UseKeyModal.spec.ts | 14 ++- .../__tests__/useModelWhitelist.spec.ts | 11 ++- frontend/src/composables/useModelWhitelist.ts | 4 + 17 files changed, 258 insertions(+), 13 deletions(-) diff --git a/backend/internal/pkg/openai/constants.go b/backend/internal/pkg/openai/constants.go index 772143ac92..bd231a414b 100644 --- a/backend/internal/pkg/openai/constants.go +++ b/backend/internal/pkg/openai/constants.go @@ -19,6 +19,8 @@ type Model struct { // DefaultModels OpenAI models list var DefaultModels = []Model{ {ID: "gpt-5.6-sol", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Sol"}, + {ID: "gpt-6-astra", Object: "model", Created: 1788480000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 Astra"}, + {ID: "gpt-6", Object: "model", Created: 1788480000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 (Astra)"}, {ID: "gpt-5.6", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 (Sol)"}, {ID: "gpt-5.6-terra", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Terra"}, {ID: "gpt-5.6-luna", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Luna"}, diff --git a/backend/internal/pkg/openai/constants_test.go b/backend/internal/pkg/openai/constants_test.go index c8f69efb66..55162d586d 100644 --- a/backend/internal/pkg/openai/constants_test.go +++ b/backend/internal/pkg/openai/constants_test.go @@ -10,6 +10,11 @@ func TestDefaultModelsIncludeBareGPT56Alias(t *testing.T) { require.Contains(t, DefaultModelIDs(), "gpt-5.6") } +func TestDefaultModelsIncludeGPT6Astra(t *testing.T) { + require.Contains(t, DefaultModelIDs(), "gpt-6-astra") + require.Contains(t, DefaultModelIDs(), "gpt-6") +} + func TestDefaultModelsPreferConcreteGPT56SolForAccountTests(t *testing.T) { require.NotEmpty(t, DefaultModels) require.Equal(t, "gpt-5.6-sol", DefaultModels[0].ID) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 1f3a346f64..85b0b54834 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -431,6 +431,20 @@ func (s *BillingService) initFallbackPricing() { SupportsCacheBreakdown: false, } + s.fallbackPrices["gpt-6-astra"] = &ModelPricing{ + InputPricePerToken: 10e-6, + InputPricePerTokenPriority: 20e-6, + OutputPricePerToken: 50e-6, + OutputPricePerTokenPriority: 100e-6, + CacheCreationPricePerToken: 12.5e-6, + CacheCreationPricePerTokenPriority: 25e-6, + CacheReadPricePerToken: 1e-6, + CacheReadPricePerTokenPriority: 2e-6, + LongContextInputThreshold: 272_000, + LongContextInputMultiplier: 2, + LongContextOutputMultiplier: 1.5, + } + // OpenAI GPT-5.6 官方价格(USD/token)。缓存写入为输入价的 1.25 倍。 s.fallbackPrices["gpt-5.6-sol"] = &ModelPricing{ InputPricePerToken: 5e-6, @@ -976,6 +990,8 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { // OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。 if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" { switch normalized { + case "gpt-6-astra": + return s.fallbackPrices["gpt-6-astra"] case "gpt-5.6-sol": return s.fallbackPrices["gpt-5.6-sol"] case "gpt-5.6-terra": @@ -1653,7 +1669,7 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing // 档的模型(如 gpt-5.5-pro、gpt-5.4-mini/nano)返回 0。 func openAIModelFastPricingRatio(normalized string) float64 { switch normalized { - case "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna": + case "gpt-6-astra", "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna": return 2.0 case "gpt-5.5": return 2.5 diff --git a/backend/internal/service/openai_codex_models_service.go b/backend/internal/service/openai_codex_models_service.go index 6c97eb9b84..ac46882c7f 100644 --- a/backend/internal/service/openai_codex_models_service.go +++ b/backend/internal/service/openai_codex_models_service.go @@ -311,6 +311,7 @@ const ( configuredCodexGrokContext = 500_000 configuredCodexGrokBuildContext = 256_000 configuredCodexGPT56MaxContext = 872_000 + configuredCodexGPT6AstraContext = 1_050_000 configuredCodexToolOutputMaxTokens = 10_000 ) @@ -490,6 +491,10 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr if isOpenAIGPT56Model(modelID) { descriptor.MaxContextWindow = configuredCodexGPT56MaxContext } + if isOpenAIGPT6AstraModel(modelID) { + descriptor.ContextWindow = configuredCodexGPT6AstraContext + descriptor.MaxContextWindow = configuredCodexGPT6AstraContext + } } if SupportsVerbosity(modelID) { defaultVerbosity := "low" @@ -503,6 +508,9 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr func configuredCodexSupportsPriorityServiceTier(modelID string) bool { normalized := canonicalizeOpenAIModelAliasSpelling(modelID) + if isOpenAIGPT6AstraModel(normalized) { + return true + } for _, family := range []string{"gpt-5.4", "gpt-5.5", "gpt-5.6"} { if normalized == family || strings.HasPrefix(normalized, family+"-") { return true @@ -567,7 +575,7 @@ func configuredCodexGPTReasoningLevels(modelID string) []configuredCodexReasonin {Effort: "xhigh", Description: "Extra-high reasoning depth for difficult tasks"}, } normalized := getNormalizedCodexModel(modelID) - if isOpenAIGPT56Model(modelID) { + if isOpenAIGPT6AstraModel(modelID) || isOpenAIGPT56Model(modelID) { levels = append(levels, configuredCodexReasoningLevel{ Effort: "max", Description: "Maximum reasoning depth for complex tasks", @@ -592,12 +600,13 @@ func isOpenAICodexGPTModel(modelID string) bool { func isOpenAICodexReasoningGPTModel(modelID string) bool { normalized := canonicalizeOpenAIModelAliasSpelling(modelID) - return strings.HasPrefix(normalized, "gpt-5") + return isOpenAIGPT6AstraModel(normalized) || strings.HasPrefix(normalized, "gpt-5") } func isOpenAICodexImageInputModel(modelID string) bool { normalized := canonicalizeOpenAIModelAliasSpelling(modelID) - return strings.HasPrefix(normalized, "gpt-5") || + return isOpenAIGPT6AstraModel(normalized) || + strings.HasPrefix(normalized, "gpt-5") || strings.HasPrefix(normalized, "gpt-4o") || strings.HasPrefix(normalized, "gpt-4.1") || strings.HasPrefix(normalized, "gpt-4.5") || @@ -1834,6 +1843,7 @@ func CodexModelsManifestETag(body []byte) string { } var apiKeyCodexModelsWithoutResponsesLite = map[string]struct{}{ + "gpt-6-astra": {}, "gpt-5.6-sol": {}, "gpt-5.6-terra": {}, "gpt-5.6-luna": {}, diff --git a/backend/internal/service/openai_codex_models_service_test.go b/backend/internal/service/openai_codex_models_service_test.go index 49b5f4e462..ed36119b5f 100644 --- a/backend/internal/service/openai_codex_models_service_test.go +++ b/backend/internal/service/openai_codex_models_service_test.go @@ -349,6 +349,25 @@ func TestNewConfiguredCodexModelDescriptorUsesProviderMetadataAndSafeFallback(t require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(gpt56Luna.SupportedReasoningLevels)) require.Equal(t, "medium", *gpt56Luna.DefaultReasoningLevel) + astra := newConfiguredCodexModelDescriptor("gpt-6-astra") + require.Equal(t, "GPT-6 Astra", astra.DisplayName) + require.NotNil(t, astra.DefaultReasoningLevel) + require.Equal(t, "medium", *astra.DefaultReasoningLevel) + require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(astra.SupportedReasoningLevels)) + require.NotContains(t, astra.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "none"}) + require.NotContains(t, astra.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "minimal"}) + require.Len(t, astra.ServiceTiers, 1) + require.Equal(t, "priority", astra.ServiceTiers[0].ID) + require.True(t, astra.SupportsParallelToolCalls) + require.True(t, astra.SupportVerbosity) + require.Equal(t, int64(1_050_000), astra.ContextWindow) + require.Equal(t, int64(1_050_000), astra.MaxContextWindow) + require.Equal(t, configuredCodexTruncationPolicy{Mode: "tokens", Limit: 10_000}, astra.TruncationPolicy) + gpt6 := newConfiguredCodexModelDescriptor("gpt-6") + require.Equal(t, "GPT-6 (Astra)", gpt6.DisplayName) + require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(gpt6.SupportedReasoningLevels)) + require.Equal(t, int64(1_050_000), gpt6.ContextWindow) + gpt55 := newConfiguredCodexModelDescriptor("gpt-5.5") require.Equal(t, "GPT-5.5", gpt55.DisplayName) require.NotNil(t, gpt55.DefaultReasoningLevel) @@ -2129,12 +2148,12 @@ func TestCompleteAPIKeyCodexModelsManifestForClientMarksOnlyOfficialVisionGPTIma t.Parallel() svc := &OpenAIGatewayService{} - manifest := &CodexModelsManifest{Body: []byte(`{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-4o"},{"slug":"gpt-3.5-turbo"},{"slug":"gpt-4"}]}`)} + manifest := &CodexModelsManifest{Body: []byte(`{"models":[{"slug":"gpt-6-astra"},{"slug":"gpt-5.6-sol"},{"slug":"gpt-4o"},{"slug":"gpt-3.5-turbo"},{"slug":"gpt-4"}]}`)} account := newCodexModelsAPIKeyTestAccount("") require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account)) models := decodeCodexManifestModels(t, manifest.Body) - require.Len(t, models, 4) + require.Len(t, models, 5) bySlug := make(map[string]map[string]any, len(models)) for _, model := range models { @@ -2142,7 +2161,7 @@ func TestCompleteAPIKeyCodexModelsManifestForClientMarksOnlyOfficialVisionGPTIma require.True(t, ok) bySlug[slug] = model } - for _, slug := range []string{"gpt-5.6-sol", "gpt-4o"} { + for _, slug := range []string{"gpt-6-astra", "gpt-5.6-sol", "gpt-4o"} { require.Equal(t, []any{"text", "image"}, bySlug[slug]["input_modalities"]) require.Equal(t, true, bySlug[slug]["supports_image_detail_original"]) } @@ -2173,8 +2192,8 @@ func TestAdjustAPIKeyCodexModelsManifest(t *testing.T) { }{ { name: "affected models disable responses lite and preserve unknown fields", - body: `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":true,"unknown_model":{"enabled":true}},{"slug":"gpt-5.6-terra","use_responses_lite":true},{"slug":"gpt-5.6-luna","use_responses_lite":true}],"unknown_top":{"version":1}}`, - want: `{"models":[{"slug":"gpt-5.6-sol","unknown_model":{"enabled":true},"use_responses_lite":false},{"slug":"gpt-5.6-terra","use_responses_lite":false},{"slug":"gpt-5.6-luna","use_responses_lite":false}],"unknown_top":{"version":1}}`, + body: `{"models":[{"slug":"gpt-6-astra","use_responses_lite":true},{"slug":"gpt-5.6-sol","use_responses_lite":true,"unknown_model":{"enabled":true}},{"slug":"gpt-5.6-terra","use_responses_lite":true},{"slug":"gpt-5.6-luna","use_responses_lite":true}],"unknown_top":{"version":1}}`, + want: `{"models":[{"slug":"gpt-6-astra","use_responses_lite":false},{"slug":"gpt-5.6-sol","unknown_model":{"enabled":true},"use_responses_lite":false},{"slug":"gpt-5.6-terra","use_responses_lite":false},{"slug":"gpt-5.6-luna","use_responses_lite":false}],"unknown_top":{"version":1}}`, }, { name: "unaffected model unchanged", diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index fbd14ba967..ef722a93c1 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -11,6 +11,7 @@ import ( ) var codexModelMap = map[string]string{ + "gpt-6-astra": "gpt-6-astra", "gpt-5.6-sol": "gpt-5.6-sol", "gpt-5.6-terra": "gpt-5.6-terra", "gpt-5.6-luna": "gpt-5.6-luna", diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 96e7715d5d..899ba7d933 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -1361,6 +1361,10 @@ func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) { func TestNormalizeCodexModel_Gpt53(t *testing.T) { cases := map[string]string{ + "gpt-6-astra": "gpt-6-astra", + "openai/gpt-6-astra": "gpt-6-astra", + "gpt-6": "gpt-6-astra", + "openai/gpt-6": "gpt-6-astra", "gpt-5.4": "gpt-5.4", "gpt5.5": "gpt-5.5", "openai/gpt5.5": "gpt-5.5", diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index 46f69b54d6..af9b79004c 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -2162,7 +2162,7 @@ func normalizeOpenAIReasoningEffortForModel(raw, model string) string { // supportsOpenAIReasoningEffortMax reports model families whose upstream scale // has a distinct max level. Other models keep the legacy max -> xhigh behavior. func supportsOpenAIReasoningEffortMax(model string) bool { - if isOpenAIGPT56Model(model) { + if isOpenAIGPT6AstraModel(model) || isOpenAIGPT56Model(model) { return true } diff --git a/backend/internal/service/openai_gpt56_max_test.go b/backend/internal/service/openai_gpt56_max_test.go index 9dc865295c..a0d1573304 100644 --- a/backend/internal/service/openai_gpt56_max_test.go +++ b/backend/internal/service/openai_gpt56_max_test.go @@ -14,13 +14,14 @@ import ( "github.com/tidwall/gjson" ) -func TestNormalizeOpenAIReasoningEffortForGPT56(t *testing.T) { +func TestNormalizeOpenAIReasoningEffortForMaxCapableModels(t *testing.T) { tests := []struct { name string raw string model string want string }{ + {name: "Astra 保留 max", raw: "max", model: "gpt-6-astra", want: "max"}, {name: "Sol 保留 max", raw: "max", model: "gpt-5.6-sol", want: "max"}, {name: "Terra 保留 max", raw: "max", model: "openai/gpt-5.6-terra", want: "max"}, {name: "Luna 后缀保留 max", raw: "max", model: "gpt-5.6-luna-2026-07-09", want: "max"}, diff --git a/backend/internal/service/openai_model_alias.go b/backend/internal/service/openai_model_alias.go index 4ddf8110c3..6de0913066 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 isOpenAIGPT6AstraModel(normalized): + return "gpt-6-astra" case strings.Contains(normalized, "gpt-5.6-sol"): return "gpt-5.6-sol" case strings.Contains(normalized, "gpt-5.6-terra"): @@ -106,6 +108,11 @@ func normalizeKnownOpenAICodexModel(model string) string { } } +func isOpenAIGPT6AstraModel(model string) bool { + normalized := canonicalizeOpenAIModelAliasSpelling(model) + return normalized == "gpt-6" || normalized == "gpt-6-astra" +} + // isOpenAIGPT56Model 判断是否 GPT-5.6 系列模型;入参可为原始模型名 // (含大小写/路径/后缀变体)或已归一化的基名,两者均能正确识别。 func isOpenAIGPT56Model(model string) bool { diff --git a/backend/internal/service/openai_model_alias_test.go b/backend/internal/service/openai_model_alias_test.go index 4cb27206c9..9ac5636892 100644 --- a/backend/internal/service/openai_model_alias_test.go +++ b/backend/internal/service/openai_model_alias_test.go @@ -6,6 +6,12 @@ import ( "github.com/stretchr/testify/require" ) +func TestNormalizeKnownOpenAICodexModelGPT6Astra(t *testing.T) { + for _, model := range []string{"gpt-6-astra", "openai/gpt-6-astra", "gpt-6", "openai/gpt-6"} { + require.Equal(t, "gpt-6-astra", normalizeKnownOpenAICodexModel(model)) + } +} + func TestNormalizeKnownOpenAICodexModel_BareGPT56RoutesToSol(t *testing.T) { tests := map[string]string{ "gpt-5.6": "gpt-5.6-sol", diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index 826f6a7803..79790d6fb4 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -44,6 +44,23 @@ var ( Mode: "chat", SupportsPromptCaching: true, } + openAIGPT6AstraFallbackPricing = &LiteLLMModelPricing{ + InputCostPerToken: 1e-05, + InputCostPerTokenPriority: 2e-05, + OutputCostPerToken: 5e-05, + OutputCostPerTokenPriority: 1e-04, + CacheCreationInputTokenCost: 1.25e-05, + CacheCreationInputTokenCostPriority: 2.5e-05, + CacheReadInputTokenCost: 1e-06, + CacheReadInputTokenCostPriority: 2e-06, + LongContextInputTokenThreshold: 272_000, + LongContextInputCostMultiplier: 2, + LongContextOutputCostMultiplier: 1.5, + SupportsServiceTier: true, + LiteLLMProvider: "openai", + Mode: "chat", + SupportsPromptCaching: true, + } openAIGPT56SolFallbackPricing = &LiteLLMModelPricing{ InputCostPerToken: 5e-06, InputCostPerTokenPriority: 1e-05, @@ -1100,6 +1117,9 @@ func normalizeModelNameForPricing(model string) string { model = strings.TrimLeft(model, "/") if canonical := canonicalizeOpenAIModelAliasSpelling(model); canonical != "" { + if canonical == "gpt-6" { + return "gpt-6-astra" + } if canonical == "gpt-5.6" { return "gpt-5.6-sol" } @@ -1300,6 +1320,12 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing { } } + if isOpenAIGPT6AstraModel(model) { + logger.With(zap.String("component", "service.pricing")). + Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-6-astra(static)")) + return openAIGPT6AstraFallbackPricing + } + if strings.HasPrefix(model, "gpt-5.6-sol") { logger.With(zap.String("component", "service.pricing")). Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.6-sol(static)")) diff --git a/backend/internal/service/pricing_service_test.go b/backend/internal/service/pricing_service_test.go index a02c74fe72..d65271dd16 100644 --- a/backend/internal/service/pricing_service_test.go +++ b/backend/internal/service/pricing_service_test.go @@ -77,6 +77,95 @@ func TestParsePricingData_ParsesPriorityAndServiceTierFields(t *testing.T) { require.True(t, pricing.SupportsServiceTier) } +const gpt6AstraCatalogJSON = `{ + "gpt-6-astra": { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": 1e-05, + "input_cost_per_token_priority": 2e-05, + "output_cost_per_token": 5e-05, + "output_cost_per_token_priority": 1e-04, + "cache_creation_input_token_cost": 1.25e-05, + "cache_creation_input_token_cost_priority": 2.5e-05, + "cache_read_input_token_cost": 1e-06, + "cache_read_input_token_cost_priority": 2e-06, + "input_cost_per_token_above_272k_tokens": 2e-05, + "output_cost_per_token_above_272k_tokens": 7.5e-05, + "cache_creation_input_token_cost_above_272k_tokens": 2.5e-05, + "cache_read_input_token_cost_above_272k_tokens": 2e-06 + } +}` + +func TestBillingServiceGPT6AstraUsesOfficialPricingAcrossTiersAndLongContext(t *testing.T) { + svc := NewBillingService(&config.Config{}, newStubPricingServiceFromJSON(t, gpt6AstraCatalogJSON)) + boundaryTokens := UsageTokens{InputTokens: 100_000, CacheCreationTokens: 100_000, CacheReadTokens: 72_000, OutputTokens: 10} + boundary, err := svc.CalculateCost("gpt-6-astra", boundaryTokens, 1) + require.NoError(t, err) + require.False(t, boundary.LongContextBillingApplied) + require.InDelta(t, 100_000*10e-6, boundary.InputCost, 1e-12) + require.InDelta(t, 100_000*12.5e-6, boundary.CacheCreationCost, 1e-12) + require.InDelta(t, 72_000*1e-6, boundary.CacheReadCost, 1e-12) + require.InDelta(t, 10*50e-6, boundary.OutputCost, 1e-12) + + tokens := UsageTokens{InputTokens: 100_000, CacheCreationTokens: 100_000, CacheReadTokens: 73_000, OutputTokens: 10} + tiers := []struct { + name string + serviceTier string + priceScale float64 + }{ + {name: "standard", priceScale: 1}, + {name: "fast", serviceTier: "priority", priceScale: 2}, + {name: "flex", serviceTier: "flex", priceScale: 0.5}, + } + for _, tier := range tiers { + t.Run(tier.name, func(t *testing.T) { + cost, err := svc.CalculateCostWithServiceTier("gpt-6-astra", tokens, 1, tier.serviceTier) + require.NoError(t, err) + require.True(t, cost.LongContextBillingApplied) + require.InDelta(t, 100_000*10e-6*tier.priceScale*2, cost.InputCost, 1e-12) + require.InDelta(t, 100_000*12.5e-6*tier.priceScale*2, cost.CacheCreationCost, 1e-12) + require.InDelta(t, 73_000*1e-6*tier.priceScale*2, cost.CacheReadCost, 1e-12) + require.InDelta(t, 10*50e-6*tier.priceScale*1.5, cost.OutputCost, 1e-12) + }) + } +} + +func TestGPT6AstraDedicatedFallbacksUseOfficialRates(t *testing.T) { + tests := []struct { + name string + svc *BillingService + }{ + {name: "pricing_service", svc: NewBillingService(&config.Config{}, &PricingService{pricingData: map[string]*LiteLLMModelPricing{}})}, + {name: "billing_service", svc: NewBillingService(&config.Config{}, nil)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pricing, err := tt.svc.GetModelPricing("gpt-6-astra") + require.NoError(t, err) + require.InDelta(t, 10e-6, pricing.InputPricePerToken, 1e-12) + require.InDelta(t, 20e-6, pricing.InputPricePerTokenPriority, 1e-12) + require.InDelta(t, 50e-6, pricing.OutputPricePerToken, 1e-12) + require.InDelta(t, 100e-6, pricing.OutputPricePerTokenPriority, 1e-12) + require.InDelta(t, 12.5e-6, pricing.CacheCreationPricePerToken, 1e-12) + require.InDelta(t, 25e-6, pricing.CacheCreationPricePerTokenPriority, 1e-12) + require.InDelta(t, 1e-6, pricing.CacheReadPricePerToken, 1e-12) + require.InDelta(t, 2e-6, pricing.CacheReadPricePerTokenPriority, 1e-12) + require.Equal(t, 272_000, pricing.LongContextInputThreshold) + require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12) + require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12) + }) + } +} + +func TestPricingServiceBareGPT6AliasUsesAstra(t *testing.T) { + astraPricing := &LiteLLMModelPricing{InputCostPerToken: 123e-6, OutputCostPerToken: 456e-6} + pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{"gpt-6-astra": astraPricing}} + for _, model := range []string{"gpt-6", "openai/gpt-6"} { + pricing := pricingSvc.GetModelPricing(model) + require.Same(t, astraPricing, pricing) + } +} + func TestBillingService_GPT56CacheWritePricingUsesOfficialMultiplier(t *testing.T) { tests := []struct { model string diff --git a/frontend/src/components/keys/UseKeyModal.vue b/frontend/src/components/keys/UseKeyModal.vue index c6d0099017..0f4e3b31e0 100644 --- a/frontend/src/components/keys/UseKeyModal.vue +++ b/frontend/src/components/keys/UseKeyModal.vue @@ -1307,6 +1307,40 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin } } const openaiModels = { + 'gpt-6': { + name: 'GPT-6 (Astra)', + limit: { + context: 1050000, + output: 128000 + }, + options: { + store: false + }, + variants: { + low: {}, + medium: {}, + high: {}, + xhigh: {}, + max: {} + } + }, + 'gpt-6-astra': { + name: 'GPT-6 Astra', + limit: { + context: 1050000, + output: 128000 + }, + options: { + store: false + }, + variants: { + low: {}, + medium: {}, + high: {}, + xhigh: {}, + max: {} + } + }, 'gpt-5.2': { name: 'GPT-5.2', limit: { diff --git a/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts b/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts index 0a31031ee2..2344b481f7 100644 --- a/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts +++ b/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts @@ -600,7 +600,7 @@ describe('UseKeyModal', () => { expect(codeBlock.text()).not.toContain('"name": "GPT-5.4 Nano"') }) - it('renders GPT-5.6 alias and max variants in OpenCode config', async () => { + it('renders GPT-5.6 and GPT-6 Astra capabilities in OpenCode config', async () => { const wrapper = mount(UseKeyModal, { props: { show: true, @@ -635,6 +635,18 @@ describe('UseKeyModal', () => { expect(models[model].variants).toHaveProperty('xhigh') } expect(models['gpt-5.6'].name).toBe('GPT-5.6 (Sol)') + expect(models['gpt-6']).toEqual({ + name: 'GPT-6 (Astra)', + limit: { context: 1050000, output: 128000 }, + options: { store: false }, + variants: { low: {}, medium: {}, high: {}, xhigh: {}, max: {} } + }) + expect(models['gpt-6-astra']).toEqual({ + name: 'GPT-6 Astra', + limit: { context: 1050000, output: 128000 }, + options: { store: false }, + variants: { low: {}, medium: {}, high: {}, xhigh: {}, max: {} } + }) }) it('renders Claude Fable 5 OpenCode config with adaptive thinking', async () => { diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index a4198a7afb..df9f347d5a 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -4,7 +4,7 @@ vi.mock('@/api/admin/accounts', () => ({ getAntigravityDefaultModelMapping: vi.fn() })) -import { buildModelMappingObject, getModelsByPlatform, splitModelMappingObject } from '../useModelWhitelist' +import { buildModelMappingObject, getModelsByPlatform, getPresetMappingsByPlatform, splitModelMappingObject } from '../useModelWhitelist' describe('useModelWhitelist', () => { it('openai 模型列表包含 GPT-5.4 官方快照', () => { @@ -15,6 +15,15 @@ describe('useModelWhitelist', () => { expect(models).toContain('gpt-5.4-2026-03-05') expect(models).toContain('codex-auto-review') expect(models).toContain('gpt-5.6') + expect(models).toContain('gpt-6') + expect(models).toContain('gpt-6-astra') + }) + + it('openai 预设映射包含 GPT-6 别名和 Astra', () => { + expect(getPresetMappingsByPlatform('openai')).toEqual(expect.arrayContaining([ + expect.objectContaining({ label: 'GPT-6', from: 'gpt-6', to: 'gpt-6' }), + expect.objectContaining({ label: 'GPT-6 Astra', from: 'gpt-6-astra', to: 'gpt-6-astra' }) + ])) }) it('openai 模型列表不再暴露已下线的 ChatGPT 登录 Codex 模型', () => { diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index b17427a8a2..5fdbbeb48f 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -4,6 +4,8 @@ // OpenAI const openaiModels = [ + // GPT-6 系列 + 'gpt-6', 'gpt-6-astra', // GPT-5.2 系列 'gpt-5.2', 'gpt-5.2-2025-12-11', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'gpt-5.2-pro-2025-12-11', @@ -290,6 +292,8 @@ const openaiPresetMappings = [ { label: 'o3', from: 'o3', to: 'o3', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, { label: 'GPT-5.3 Codex Spark', from: 'gpt-5.3-codex-spark', to: 'gpt-5.3-codex-spark', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' }, { label: 'GPT-5.2', from: 'gpt-5.2', to: 'gpt-5.2', color: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400' }, + { label: 'GPT-6', from: 'gpt-6', to: 'gpt-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, + { label: 'GPT-6 Astra', from: 'gpt-6-astra', to: 'gpt-6-astra', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' }, { label: 'GPT-5.6', from: 'gpt-5.6', to: 'gpt-5.6', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, { label: 'GPT-5.6 Sol', from: 'gpt-5.6-sol', to: 'gpt-5.6-sol', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, { label: 'GPT-5.6 Terra', from: 'gpt-5.6-terra', to: 'gpt-5.6-terra', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' },