From cccba9a82e6100c34ebb679b1bab6998b1185b58 Mon Sep 17 00:00:00 2001 From: Heatherm Huang Date: Thu, 9 Jul 2026 14:26:10 +0800 Subject: [PATCH] Add official Grok 4.5 support --- .../handler/openai_gateway_handler_test.go | 2 +- backend/internal/pkg/xai/models.go | 9 ++-- backend/internal/pkg/xai/oauth_test.go | 7 ++- backend/internal/service/billing_service.go | 12 ++++- .../internal/service/billing_service_test.go | 16 +++++++ .../internal/service/openai_gateway_grok.go | 10 +++++ .../service/openai_gateway_grok_test.go | 44 +++++++++++++++++++ .../service/openai_messages_dispatch_test.go | 6 +-- .../__tests__/useModelWhitelist.spec.ts | 27 ++++++++++++ frontend/src/composables/useModelWhitelist.ts | 8 +++- 10 files changed, 130 insertions(+), 11 deletions(-) diff --git a/backend/internal/handler/openai_gateway_handler_test.go b/backend/internal/handler/openai_gateway_handler_test.go index c4ccb5a024..b7f43079ef 100644 --- a/backend/internal/handler/openai_gateway_handler_test.go +++ b/backend/internal/handler/openai_gateway_handler_test.go @@ -440,7 +440,7 @@ func TestResolveOpenAIMessagesDispatchMappedModel(t *testing.T) { Platform: service.PlatformGrok, }, } - require.Equal(t, "grok-4.3", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-sonnet-4-5")) + require.Equal(t, "grok-4.5", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-sonnet-4-5")) require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(apiKey, "grok")) }) diff --git a/backend/internal/pkg/xai/models.go b/backend/internal/pkg/xai/models.go index a5b800cf2c..a42f9b1871 100644 --- a/backend/internal/pkg/xai/models.go +++ b/backend/internal/pkg/xai/models.go @@ -10,6 +10,7 @@ type Model struct { } var defaultModels = []Model{ + {ID: "grok-4.5", Object: "model", OwnedBy: "xai", DisplayName: "Grok 4.5"}, {ID: "grok-4.3", Object: "model", OwnedBy: "xai", DisplayName: "Grok 4.3"}, {ID: "grok-build-0.1", Object: "model", OwnedBy: "xai", DisplayName: "Grok Build 0.1"}, {ID: "grok-composer-2.5-fast", Object: "model", OwnedBy: "xai", DisplayName: "Grok Composer 2.5 Fast"}, @@ -40,13 +41,15 @@ func DefaultModelIDs() []string { } func DefaultModelMapping() map[string]string { - mapping := make(map[string]string, len(defaultModels)+3) + mapping := make(map[string]string, len(defaultModels)+5) for _, model := range defaultModels { mapping[model.ID] = model.ID } - mapping["grok"] = "grok-4.3" - mapping["grok-latest"] = "grok-4.3" + mapping["grok"] = "grok-4.5" + mapping["grok-latest"] = "grok-4.5" + mapping["grok-4.5-latest"] = "grok-4.5" mapping["grok-build"] = "grok-build-0.1" + mapping["grok-build-latest"] = "grok-4.5" mapping["grok-composer"] = "grok-composer-2.5-fast" mapping["grok-4.20-reasoning"] = "grok-4.20-0309-reasoning" mapping["grok-4.20-non-reasoning"] = "grok-4.20-0309-non-reasoning" diff --git a/backend/internal/pkg/xai/oauth_test.go b/backend/internal/pkg/xai/oauth_test.go index 28609a08fa..1eea83640b 100644 --- a/backend/internal/pkg/xai/oauth_test.go +++ b/backend/internal/pkg/xai/oauth_test.go @@ -207,9 +207,12 @@ func TestDefaultModelMappingIncludesGrokAliases(t *testing.T) { t.Parallel() mapping := DefaultModelMapping() - require.Equal(t, "grok-4.3", mapping["grok"]) - require.Equal(t, "grok-4.3", mapping["grok-latest"]) + require.Equal(t, "grok-4.5", mapping["grok"]) + require.Equal(t, "grok-4.5", mapping["grok-latest"]) + require.Equal(t, "grok-4.5", mapping["grok-4.5"]) + require.Equal(t, "grok-4.5", mapping["grok-4.5-latest"]) require.Equal(t, "grok-build-0.1", mapping["grok-build"]) + require.Equal(t, "grok-4.5", mapping["grok-build-latest"]) require.Equal(t, "grok-composer-2.5-fast", mapping["grok-composer"]) require.Equal(t, "grok-4.20-0309-reasoning", mapping["grok-4.20-reasoning"]) require.Equal(t, "grok-4.20-0309-non-reasoning", mapping["grok-4.20-non-reasoning"]) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 4c265aed3c..b3d34eb175 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -512,6 +512,14 @@ func (s *BillingService) initFallbackPricing() { SupportsCacheBreakdown: false, } + // xAI Grok 4.5 (official docs: $2 input / $0.50 cached input / $6 output per MTok) + s.fallbackPrices["grok-4.5"] = &ModelPricing{ + InputPricePerToken: 2e-6, + OutputPricePerToken: 6e-6, + CacheReadPricePerToken: 0.5e-6, + SupportsCacheBreakdown: false, + } + // xAI Grok 4.3 (official docs: $1.25 input / $2.50 output per MTok) s.fallbackPrices["grok-4.3"] = &ModelPricing{ InputPricePerToken: 1.25e-6, @@ -696,7 +704,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { } switch modelLower { - case "grok", "grok-latest", "grok-4.3": + case "grok", "grok-latest", "grok-4.5", "grok-4.5-latest", "grok-build-latest": + return s.fallbackPrices["grok-4.5"] + case "grok-4.3": return s.fallbackPrices["grok-4.3"] case "grok-build", "grok-build-0.1": return s.fallbackPrices["grok-build-0.1"] diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 92c143c6ff..4adacd6f8a 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -963,6 +963,22 @@ func TestCalculateCostWithLongContext_PropagatesError(t *testing.T) { require.Contains(t, err.Error(), "pricing not found") } +func TestGetModelPricing_Grok45OfficialFallback(t *testing.T) { + svc := newTestBillingService() + + for _, model := range []string{"grok", "grok-latest", "grok-4.5", "grok-4.5-latest", "grok-build-latest"} { + model := model + t.Run(model, func(t *testing.T) { + pricing, err := svc.GetModelPricing(model) + require.NoError(t, err) + require.InDelta(t, 2e-6, pricing.InputPricePerToken, 1e-12) + require.InDelta(t, 6e-6, pricing.OutputPricePerToken, 1e-12) + require.InDelta(t, 0.5e-6, pricing.CacheReadPricePerToken, 1e-12) + require.False(t, pricing.SupportsCacheBreakdown) + }) + } +} + func TestCalculateCost_SupportsCacheBreakdown(t *testing.T) { svc := &BillingService{ cfg: &config.Config{}, diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index 4a0ad06d46..19e44fc4d9 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -153,6 +153,16 @@ func patchGrokResponsesBody(body []byte, upstreamModel string) ([]byte, error) { } } } + if strings.EqualFold(upstreamModel, "grok-4.5") { + for _, unsupportedField := range []string{"presence_penalty", "presencePenalty", "frequency_penalty", "frequencyPenalty", "stop"} { + if gjson.GetBytes(out, unsupportedField).Exists() { + out, err = sjson.DeleteBytes(out, unsupportedField) + if err != nil { + return nil, err + } + } + } + } out, err = sanitizeGrokResponsesUnsupportedFields(out) if err != nil { return nil, err diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index f6aa4b6cd1..b0223c8580 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -41,6 +41,50 @@ func TestPatchGrokResponsesBodySetsMappedModelAndDropsUnsupportedFields(t *testi require.Equal(t, "high", gjson.GetBytes(patched, "reasoning.effort").String()) } +func TestPatchGrokResponsesBodyDropsGrok45ReasoningUnsupportedFields(t *testing.T) { + t.Parallel() + + body := []byte(`{ + "model": "grok-latest", + "input": "hello", + "presence_penalty": 0.1, + "presencePenalty": 0.2, + "frequency_penalty": 0.3, + "frequencyPenalty": 0.4, + "stop": ["done"] + }`) + + patched, err := patchGrokResponsesBody(body, "grok-4.5") + require.NoError(t, err) + require.True(t, json.Valid(patched)) + require.Equal(t, "grok-4.5", gjson.GetBytes(patched, "model").String()) + require.False(t, gjson.GetBytes(patched, "presence_penalty").Exists()) + require.False(t, gjson.GetBytes(patched, "presencePenalty").Exists()) + require.False(t, gjson.GetBytes(patched, "frequency_penalty").Exists()) + require.False(t, gjson.GetBytes(patched, "frequencyPenalty").Exists()) + require.False(t, gjson.GetBytes(patched, "stop").Exists()) +} + +func TestPatchGrokResponsesBodyKeepsPenaltyAndStopFieldsForNon45Models(t *testing.T) { + t.Parallel() + + body := []byte(`{ + "model": "grok-4.3", + "input": "hello", + "presence_penalty": 0.1, + "frequency_penalty": 0.2, + "stop": ["done"] + }`) + + patched, err := patchGrokResponsesBody(body, "grok-4.3") + require.NoError(t, err) + require.True(t, json.Valid(patched)) + require.Equal(t, "grok-4.3", gjson.GetBytes(patched, "model").String()) + require.Equal(t, 0.1, gjson.GetBytes(patched, "presence_penalty").Float()) + require.Equal(t, 0.2, gjson.GetBytes(patched, "frequency_penalty").Float()) + require.Len(t, gjson.GetBytes(patched, "stop").Array(), 1) +} + func TestPatchGrokResponsesBodyDropsNestedUnsupportedFields(t *testing.T) { t.Parallel() diff --git a/backend/internal/service/openai_messages_dispatch_test.go b/backend/internal/service/openai_messages_dispatch_test.go index e0b8ab0aa0..bafd36449b 100644 --- a/backend/internal/service/openai_messages_dispatch_test.go +++ b/backend/internal/service/openai_messages_dispatch_test.go @@ -31,9 +31,9 @@ func TestGroupResolveMessagesDispatchModel_GrokMapsClaudeFamilyToGrok(t *testing group := &Group{Platform: PlatformGrok} - require.Equal(t, "grok-4.3", group.ResolveMessagesDispatchModel("claude-sonnet-4-5")) - require.Equal(t, "grok-4.3", group.ResolveMessagesDispatchModel("claude-opus-4-6")) - require.Equal(t, "grok-4.3", group.ResolveMessagesDispatchModel("claude-haiku-4-5")) + require.Equal(t, "grok-4.5", group.ResolveMessagesDispatchModel("claude-sonnet-4-5")) + require.Equal(t, "grok-4.5", group.ResolveMessagesDispatchModel("claude-opus-4-6")) + require.Equal(t, "grok-4.5", group.ResolveMessagesDispatchModel("claude-haiku-4-5")) require.Empty(t, group.ResolveMessagesDispatchModel("grok")) require.Empty(t, group.ResolveMessagesDispatchModel("gpt-5.3-codex")) } diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index d7e70e309d..d34d7113bb 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -42,6 +42,33 @@ describe('useModelWhitelist', () => { expect(getModelsByPlatform('antigravity')).toContain('claude-opus-4-8') }) + it('xAI 模型列表包含 Grok 4.5 官方模型和别名', () => { + const models = getModelsByPlatform('grok') + + expect(models).toContain('grok-4.5') + expect(models).toContain('grok-4.5-latest') + expect(models).toContain('grok-build-latest') + }) + + it('combined 模式支持 Grok 4.5 官方别名映射', () => { + const mapping = buildModelMappingObject( + 'combined', + ['grok-4.5'], + [ + { from: 'grok-latest', to: 'grok-4.5' }, + { from: 'grok-4.5-latest', to: 'grok-4.5' }, + { from: 'grok-build-latest', to: 'grok-4.5' } + ] + ) + + expect(mapping).toEqual({ + 'grok-4.5': 'grok-4.5', + 'grok-latest': 'grok-4.5', + 'grok-4.5-latest': 'grok-4.5', + 'grok-build-latest': 'grok-4.5' + }) + }) + it('gemini 模型列表包含原生生图模型', () => { const models = getModelsByPlatform('gemini') diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 28bc1d28ad..2f8b7486ac 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -135,6 +135,7 @@ const metaModels = [ // xAI Grok const xaiModels = [ + 'grok-4.5', 'grok-4.3', 'grok-build-0.1', 'grok-composer-2.5-fast', @@ -143,7 +144,9 @@ const xaiModels = [ 'grok-4.20-multi-agent-0309', 'grok', 'grok-latest', + 'grok-4.5-latest', 'grok-build', + 'grok-build-latest', 'grok-composer', 'grok-4.20-reasoning', 'grok-4.20-non-reasoning', @@ -296,9 +299,12 @@ const geminiPresetMappings = [ ] const grokPresetMappings = [ + { label: 'Grok 4.5', from: 'grok-4.5', to: 'grok-4.5', color: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800/50 dark:text-slate-300' }, { label: 'Grok 4.3', from: 'grok-4.3', to: 'grok-4.3', color: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800/50 dark:text-slate-300' }, - { label: 'Grok Latest', from: 'grok-latest', to: 'grok-4.3', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, + { label: 'Grok Latest', from: 'grok-latest', to: 'grok-4.5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, + { label: '4.5 Latest', from: 'grok-4.5-latest', to: 'grok-4.5', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' }, { label: 'Build 0.1', from: 'grok-build', to: 'grok-build-0.1', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, + { label: 'Build Latest', from: 'grok-build-latest', to: 'grok-4.5', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' }, { label: 'Composer 2.5', from: 'grok-composer', to: 'grok-composer-2.5-fast', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' }, { label: '4.20 Reasoning', from: 'grok-4.20-reasoning', to: 'grok-4.20-0309-reasoning', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, { label: '4.20 Non Reasoning', from: 'grok-4.20-non-reasoning', to: 'grok-4.20-0309-non-reasoning', color: 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' },