From a04ce49016ec2446370d6e0e430d2cd89e7a6654 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Thu, 13 Aug 2026 01:23:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20grok-4.6=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E3=80=81=E5=AE=98=E6=96=B9=E5=AE=9A=E4=BB=B7?= =?UTF-8?q?=E4=B8=8E=E8=AF=B7=E6=B1=82=E8=B7=AF=E5=BE=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 官方目录价:<200k 为 $2 / 缓存读取 $0.50 / $6,≥200k 全部 2 倍。 原先没有独立价卡,/models 宣称可配 reasoning 但请求路径会剥掉 effort。 - 目录与别名接入 grok-4.6 / grok-4.6-latest,默认文本模型仍为 grok-4.5 - 独立回退价卡含缓存读取价与 200k 长上下文倍率 - 保留 reasoning_effort;Chat→Responses 的 cache/vision 桥接同样识别 4.6 --- backend/internal/handler/gateway_handler.go | 2 +- backend/internal/pkg/xai/models.go | 3 +++ backend/internal/pkg/xai/models_test.go | 9 ++++++++ backend/internal/pkg/xai/oauth_test.go | 2 ++ backend/internal/service/billing_service.go | 15 ++++++++++++ .../internal/service/billing_service_test.go | 23 +++++++++++++++++-- .../internal/service/openai_gateway_grok.go | 3 ++- .../openai_gateway_grok_chat_bridge.go | 14 +++++++++-- .../openai_gateway_grok_chat_bridge_test.go | 3 +++ .../service/openai_gateway_grok_test.go | 2 ++ .../__tests__/useModelWhitelist.spec.ts | 2 ++ frontend/src/composables/useModelWhitelist.ts | 3 +++ 12 files changed, 75 insertions(+), 6 deletions(-) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 1e2d0fbe3d..07315d5efc 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -1249,7 +1249,7 @@ func writeGrokModelsList(c *gin.Context, modelIDs []string) { func grokModelSupportsConfigurableReasoning(modelID string) bool { switch strings.ToLower(strings.TrimSpace(modelID)) { - case "grok-4.5", "grok-4.5-latest", "grok", "grok-latest", "grok-build", "grok-build-latest", "grok-build-0.1": + case "grok-4.6", "grok-4.6-latest", "grok-4.5", "grok-4.5-latest", "grok", "grok-latest", "grok-build", "grok-build-latest", "grok-build-0.1": return true default: return false diff --git a/backend/internal/pkg/xai/models.go b/backend/internal/pkg/xai/models.go index c25f489317..990a6d28e0 100644 --- a/backend/internal/pkg/xai/models.go +++ b/backend/internal/pkg/xai/models.go @@ -83,6 +83,7 @@ func (o ModelMappingOptions) defaultText() string { var defaultModels = []Model{ // Text + {ID: "grok-4.6", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.6"}, {ID: "grok-4.5", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.5"}, {ID: "grok-4.3", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.3"}, {ID: "grok-3-mini", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 3 Mini"}, @@ -106,6 +107,8 @@ var defaultModels = []Model{ var grokTextResponsesModelAliases = map[string]string{ "grok": DefaultTextModel, "grok-latest": DefaultTextModel, + "grok-4.6": "grok-4.6", + "grok-4.6-latest": "grok-4.6", "grok-4.5": DefaultTextModel, "grok-4.5-latest": DefaultTextModel, "grok-4.3": "grok-4.3", diff --git a/backend/internal/pkg/xai/models_test.go b/backend/internal/pkg/xai/models_test.go index 98aef08cfc..a01d993ccb 100644 --- a/backend/internal/pkg/xai/models_test.go +++ b/backend/internal/pkg/xai/models_test.go @@ -52,11 +52,20 @@ func TestCanonicalImagineVideoModel(t *testing.T) { func TestIsGrokModelID(t *testing.T) { t.Parallel() require.True(t, IsGrokModelID("grok-4.5")) + require.True(t, IsGrokModelID("grok-4.6")) require.True(t, IsGrokModelID("x-ai/grok-4.3")) require.False(t, IsGrokModelID("gpt-5")) require.False(t, IsGrokModelID("claude-sonnet-4")) } +func TestDefaultModelsIncludesGrok46(t *testing.T) { + t.Parallel() + ids := DefaultModelIDs() + require.Contains(t, ids, "grok-4.6") + require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok-4.6")) + require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok-4.6-latest")) +} + func TestResolveGrokTextResponsesModelID(t *testing.T) { t.Parallel() require.Equal(t, "grok-4.5", ResolveGrokTextResponsesModelID("")) diff --git a/backend/internal/pkg/xai/oauth_test.go b/backend/internal/pkg/xai/oauth_test.go index 3919f4576b..90036ff8f2 100644 --- a/backend/internal/pkg/xai/oauth_test.go +++ b/backend/internal/pkg/xai/oauth_test.go @@ -352,6 +352,8 @@ func TestDefaultModelMappingIncludesGrokAliases(t *testing.T) { mapping := DefaultModelMapping() require.Equal(t, "grok-4.5", mapping["grok"]) require.Equal(t, "grok-4.5", mapping["grok-latest"]) + require.Equal(t, "grok-4.6", mapping["grok-4.6"]) + require.Equal(t, "grok-4.6", mapping["grok-4.6-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"]) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 96233b7ae9..4f4fa8e0bd 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -593,6 +593,19 @@ func (s *BillingService) initFallbackPricing() { SupportsCacheBreakdown: false, } + // xAI Grok 4.6 (docs.x.ai/developers/models: $2 input / $0.50 cached input / + // $6 output per MTok under 200k prompt tokens; ≥200k is 2× on input, + // cached input, and output). + s.fallbackPrices["grok-4.6"] = &ModelPricing{ + InputPricePerToken: 2e-6, + OutputPricePerToken: 6e-6, + CacheReadPricePerToken: 0.5e-6, + SupportsCacheBreakdown: false, + LongContextInputThreshold: 200000, + LongContextInputMultiplier: 2, + LongContextOutputMultiplier: 2, + } + // xAI Grok 4.3 (official docs: $1.25 input / $2.50 output per MTok) s.fallbackPrices["grok-4.3"] = &ModelPricing{ InputPricePerToken: 1.25e-6, @@ -805,6 +818,8 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { switch modelLower { case "grok", "grok-latest", "grok-4.5", "grok-4.5-latest", "grok-build-latest": return s.fallbackPrices["grok-4.5"] + case "grok-4.6", "grok-4.6-latest": + return s.fallbackPrices["grok-4.6"] case "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 8f322b54e9..6a30f6eeaf 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -871,8 +871,8 @@ func TestComputeTokenBreakdown_GptImage2ImageEditIssue4386(t *testing.T) { cost := svc.computeTokenBreakdown(pricing, tokens, 1.0, "", false) - wantTextInput := float64(19) * 5e-6 // 0.000095 - wantImageInput := float64(352) * 8e-6 // 0.002816 + wantTextInput := float64(19) * 5e-6 // 0.000095 + wantImageInput := float64(352) * 8e-6 // 0.002816 wantImageOutput := float64(439) * 30e-6 // 0.013170 require.InDelta(t, wantTextInput, cost.InputCost, 1e-15, "InputCost 仅含文本输入") require.InDelta(t, wantImageInput, cost.ImageInputCost, 1e-15, "图片输入按 $8/1M 独立计费") @@ -1166,6 +1166,25 @@ func TestGetModelPricing_Grok45OfficialFallback(t *testing.T) { } } +func TestGetModelPricing_Grok46OfficialFallback(t *testing.T) { + svc := newTestBillingService() + + for _, model := range []string{"grok-4.6", "grok-4.6-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.Equal(t, 200000, pricing.LongContextInputThreshold) + require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12) + require.InDelta(t, 2.0, pricing.LongContextOutputMultiplier, 1e-12) + require.False(t, pricing.SupportsCacheBreakdown) + }) + } +} + func TestGetModelPricing_GrokCatalogFallbacks(t *testing.T) { svc := newTestBillingService() diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index 3a737f72d5..514f6bef07 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -632,7 +632,8 @@ func normalizeGrokReasoningEffortValue(raw string) (string, bool) { func grokSupportsReasoningEffort(model string) bool { model = strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(model))) switch model { - case xai.DefaultTextModel, "grok-4.5-latest", "grok-4.3", "grok-4.3-latest", + case xai.DefaultTextModel, "grok-4.5-latest", "grok-4.6", "grok-4.6-latest", + "grok-4.3", "grok-4.3-latest", "grok-3-mini", "grok-3-mini-fast", "grok-4.20-0309-reasoning", "grok-4.20-reasoning", "grok-4.20-multi-agent-0309": return true diff --git a/backend/internal/service/openai_gateway_grok_chat_bridge.go b/backend/internal/service/openai_gateway_grok_chat_bridge.go index ed910eab43..88b9b0f41b 100644 --- a/backend/internal/service/openai_gateway_grok_chat_bridge.go +++ b/backend/internal/service/openai_gateway_grok_chat_bridge.go @@ -10,6 +10,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat" + "github.com/Wei-Shaw/sub2api/internal/pkg/xai" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" ) @@ -495,8 +496,17 @@ func grokChatResponsesCacheIntentBody(body []byte) ([]byte, error) { return json.Marshal(root) } +func grokChatResponsesBridgeModel(model string) bool { + switch strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(model))) { + case "grok-4.5", "grok-4.6", "grok-4.6-latest": + return true + default: + return false + } +} + func grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity string) bool { - return strings.TrimSpace(upstreamModel) == "grok-4.5" && strings.TrimSpace(cacheIdentity) != "" + return grokChatResponsesBridgeModel(upstreamModel) && strings.TrimSpace(cacheIdentity) != "" } // forwardGrokChatCompletionsViaResponses converts a strictly compatible Chat @@ -527,7 +537,7 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses( // for non-composer models, so they would be silently dropped. Route them to // Responses even when no prompt-cache identity is available. hasImageInput := openAIJSONValueMayContainImageInput(gjson.GetBytes(body, "messages")) - if !grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity) && (!hasImageInput || strings.TrimSpace(upstreamModel) != "grok-4.5") { + if !grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity) && (!hasImageInput || !grokChatResponsesBridgeModel(upstreamModel)) { return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel) } diff --git a/backend/internal/service/openai_gateway_grok_chat_bridge_test.go b/backend/internal/service/openai_gateway_grok_chat_bridge_test.go index 6c4137d9d8..373907f0d7 100644 --- a/backend/internal/service/openai_gateway_grok_chat_bridge_test.go +++ b/backend/internal/service/openai_gateway_grok_chat_bridge_test.go @@ -218,9 +218,12 @@ func TestGrokChatResponsesBridgeEligibility(t *testing.T) { func TestGrokChatResponsesRuntimeEligibility(t *testing.T) { t.Parallel() require.True(t, grokChatResponsesRuntimeEligible("grok-4.5", "isolated-id")) + require.True(t, grokChatResponsesRuntimeEligible("grok-4.6", "isolated-id")) + require.True(t, grokChatResponsesRuntimeEligible("grok-4.6-latest", "isolated-id")) require.False(t, grokChatResponsesRuntimeEligible("grok-4.3", "isolated-id")) require.False(t, grokChatResponsesRuntimeEligible("grok-4.5-build-free", "isolated-id")) require.False(t, grokChatResponsesRuntimeEligible("grok-4.5", "")) + require.False(t, grokChatResponsesRuntimeEligible("grok-4.6", "")) } func TestForwardGrokChatViaResponsesNonStreamingCachesAndReturnsChat(t *testing.T) { diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index 04edd0a4a6..243736b1c8 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -56,6 +56,8 @@ func TestPatchGrokResponsesBodySanitizesComposerReasoningParameters(t *testing.T {name: "composer legacy alias", upstreamModel: "composer-2.5"}, {name: "provider-prefixed composer", upstreamModel: "xai/grok-composer-2.5-fast"}, {name: "grok 4.5", upstreamModel: "grok-4.5", wantReasoning: true}, + {name: "grok 4.6", upstreamModel: "grok-4.6", wantReasoning: true}, + {name: "grok 4.6 latest", upstreamModel: "grok-4.6-latest", wantReasoning: true}, } bodyTemplate := []byte(`{ diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index 714164324d..9fd93e6573 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -46,6 +46,8 @@ describe('useModelWhitelist', () => { it('xAI 模型列表包含 Grok 4.5 官方模型和别名', () => { const models = getModelsByPlatform('grok') + expect(models).toContain('grok-4.6') + expect(models).toContain('grok-4.6-latest') expect(models).toContain('grok-4.5') expect(models).toContain('grok-4.5-latest') expect(models).toContain('grok-build-latest') diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 3d4c42a29e..e2c5924592 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -136,6 +136,7 @@ const metaModels = [ // xAI Grok const xaiModels = [ + 'grok-4.6', 'grok-4.5', 'grok-4.3', 'grok-build-0.1', @@ -147,6 +148,7 @@ const xaiModels = [ 'grok-4.20-multi-agent-latest', 'grok-4.3-latest', 'grok-latest', + 'grok-4.6-latest', 'grok-4.5-latest', 'grok-build-latest', 'composer-2.5', @@ -303,6 +305,7 @@ const geminiPresetMappings = [ ] const grokPresetMappings = [ + { label: 'Grok 4.6', from: 'grok-4.6', to: 'grok-4.6', color: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800/50 dark:text-slate-300' }, { 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.5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },