From 39485f2e28d39056544b3f3bb9d117cc6970b74d Mon Sep 17 00:00:00 2001 From: IanShaw <131567472+IanShaw027@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:32:22 -0700 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Grok=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=B8=8E=E5=AE=98=E6=96=B9=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/internal/pkg/xai/models.go | 12 +++--- backend/internal/pkg/xai/models_test.go | 16 +++++--- backend/internal/pkg/xai/oauth_test.go | 8 ++-- backend/internal/service/billing_service.go | 38 +++++++++++++------ .../internal/service/billing_service_test.go | 36 ++++++++++++++++-- backend/internal/service/setting_parse.go | 4 +- backend/internal/service/setting_update.go | 2 +- .../__tests__/useModelWhitelist.spec.ts | 3 +- frontend/src/composables/useModelWhitelist.ts | 2 +- 10 files changed, 87 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2c97044ddc..127653ce82 100644 --- a/README.md +++ b/README.md @@ -718,7 +718,7 @@ Sub2API supports both Grok subscription accounts through xAI OAuth and standard - Codex CLI style Responses WebSocket ingress is accepted on the Responses targets and bridged to xAI HTTP/SSE Responses upstream - Text models: `grok-4.5`, `grok-4.3`, `grok-build-0.1`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`, and `grok-4.20-multi-agent-0309` - Media targets for Grok groups: `/v1/images/generations`, `/images/generations`, `/v1/images/edits`, `/images/edits`, `/v1/videos/generations`, `/videos/generations`, `/v1/videos/edits`, `/videos/edits`, `/v1/videos/extensions`, `/videos/extensions`, `/v1/videos/{request_id}`, and `/videos/{request_id}`. Generation, editing, and extension requests require the group image-generation permission. -- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5` +- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-image-2.0`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5` - JSON image-edit and video-generation requests accept image references in `image`, `images`, `reference_images`, and `mask` objects. Use `url` for xAI-compatible payloads; the legacy `image_url` field remains accepted and is normalized to `url` before forwarding. - Out of scope for this provider: TTS, transcription, browser automation, cookies, and Grok web scraping diff --git a/backend/internal/pkg/xai/models.go b/backend/internal/pkg/xai/models.go index d392249617..6fab34dc39 100644 --- a/backend/internal/pkg/xai/models.go +++ b/backend/internal/pkg/xai/models.go @@ -51,7 +51,7 @@ type Model struct { // DefaultTextModel is the built-in fallback for empty model fields and Grok // text aliases (e.g. "grok", "grok-latest"). Operators may override the runtime // default via settings key grok_default_text_model. -const DefaultTextModel = "grok-4.5" +const DefaultTextModel = "grok-4.6" // Official Imagine model IDs (https://docs.x.ai/docs/models). const ( @@ -66,10 +66,10 @@ const ( // ModelMappingOptions controls optional expansions of the default mapping. // Cross-client wildcards (gpt-*/claude-*) default ON via settings // grok_cross_client_model_map_enabled so Codex/Claude clients keep working -// against Grok groups (map to DefaultText / grok-4.5). Operators may disable. +// against Grok groups (map to DefaultText / grok-4.6). Operators may disable. type ModelMappingOptions struct { // DefaultText is the target for empty models and optional cross-client maps. - // Empty → DefaultTextModel (grok-4.5). + // Empty → DefaultTextModel (grok-4.6). DefaultText string // EnableCrossClientMap merges gpt-*/codex-*/o*/claude-* → DefaultText. EnableCrossClientMap bool @@ -115,7 +115,7 @@ var grokTextResponsesModelAliases = map[string]string{ "grok-3-mini": "grok-3-mini", "grok-3-mini-fast": "grok-3-mini-fast", "grok-build": "grok-build-0.1", - "grok-build-latest": DefaultTextModel, + "grok-build-latest": "grok-build-0.1", "grok-build-0.1": "grok-build-0.1", "grok-composer-2.5-fast": "grok-composer-2.5-fast", "grok-composer": "grok-composer-2.5-fast", @@ -161,7 +161,7 @@ func ModelMappingWithOptions(opts ModelMappingOptions) map[string]string { } for alias, canonical := range grokTextResponsesModelAliases { // Remap aliases that pointed at DefaultTextModel constant to runtime default. - if (alias == "grok" || alias == "grok-latest" || alias == "grok-build-latest") && canonical == DefaultTextModel { + if (alias == "grok" || alias == "grok-latest") && canonical == DefaultTextModel { mapping[alias] = defaultText } else { mapping[alias] = canonical @@ -272,7 +272,7 @@ func ResolveGrokTextResponsesModelID(model string, defaultText ...string) string } normalized := strings.ToLower(StripGrokProviderPrefix(trimmed)) if canonical, ok := grokTextResponsesModelAliases[normalized]; ok { - if (normalized == "grok" || normalized == "grok-latest" || normalized == "grok-build-latest") && canonical == DefaultTextModel { + if (normalized == "grok" || normalized == "grok-latest") && canonical == DefaultTextModel { return fallback } return canonical diff --git a/backend/internal/pkg/xai/models_test.go b/backend/internal/pkg/xai/models_test.go index 0fc463814b..fb4860ee4d 100644 --- a/backend/internal/pkg/xai/models_test.go +++ b/backend/internal/pkg/xai/models_test.go @@ -12,14 +12,14 @@ func TestDefaultModelMappingExcludesCrossClientWildcards(t *testing.T) { SetRuntimeModelMappingOptions(ModelMappingOptions{}) 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"]) + require.Equal(t, "grok-4.6", mapping["grok-latest"]) require.Equal(t, "grok-build-0.1", mapping["grok-build"]) - require.Equal(t, DefaultTextModel, mapping["grok-build-latest"]) + require.Equal(t, "grok-build-0.1", mapping["grok-build-latest"]) require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine-edit"]) require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5"]) require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5-preview"]) - require.Equal(t, "grok-4.5", mapping["xai/grok"]) + require.Equal(t, "grok-4.6", mapping["xai/grok"]) // Cross-vendor wildcards must stay opt-in. _, hasGPT := mapping["gpt-*"] @@ -68,7 +68,7 @@ func TestDefaultModelsIncludesGrok46(t *testing.T) { func TestResolveGrokTextResponsesModelID(t *testing.T) { t.Parallel() - require.Equal(t, "grok-4.5", ResolveGrokTextResponsesModelID("")) + require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("")) require.Equal(t, "grok-4.3", ResolveGrokTextResponsesModelID("grok", "grok-4.3")) require.Equal(t, "grok-4.20-multi-agent-0309", ResolveGrokTextResponsesModelID("grok-4.20-multi-agent")) } @@ -78,3 +78,9 @@ func TestExplicitGrok45DoesNotFollowRuntimeDefault(t *testing.T) { require.Equal(t, "grok-4.5", ResolveGrokTextResponsesModelID("grok-4.5-latest", "grok-4.6")) require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok", "grok-4.6")) } + +func TestBareGrokAliasesFollowGrok46Default(t *testing.T) { + require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok")) + require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok-latest")) + require.Equal(t, "grok-build-0.1", ResolveGrokTextResponsesModelID("grok-build-latest")) +} diff --git a/backend/internal/pkg/xai/oauth_test.go b/backend/internal/pkg/xai/oauth_test.go index 90036ff8f2..c7b1577be4 100644 --- a/backend/internal/pkg/xai/oauth_test.go +++ b/backend/internal/pkg/xai/oauth_test.go @@ -350,14 +350,14 @@ func TestDefaultModelMappingIncludesGrokAliases(t *testing.T) { t.Cleanup(func() { SetRuntimeModelMappingOptions(original) }) SetRuntimeModelMappingOptions(ModelMappingOptions{}) 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"]) + require.Equal(t, "grok-4.6", 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"]) - require.Equal(t, "grok-4.5", mapping["grok-build-latest"]) + require.Equal(t, "grok-build-0.1", mapping["grok-build-latest"]) require.Equal(t, "grok-composer-2.5-fast", mapping["grok-composer"]) require.Equal(t, "grok-composer-2.5-fast", mapping["composer-2.5"]) require.Equal(t, "grok-4.20-0309-reasoning", mapping["grok-4.20-reasoning"]) @@ -368,7 +368,7 @@ func TestDefaultModelMappingIncludesGrokAliases(t *testing.T) { require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine-image-quality"]) require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine-edit"]) require.Equal(t, DefaultImagineVideoModel, mapping["grok-imagine-video"]) - require.Equal(t, DefaultImagineVideo15LegacyModel, mapping["grok-imagine-video-1.5"]) + require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5"]) require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5-preview"]) _, hasGPT := mapping["gpt-*"] require.False(t, hasGPT, "cross-client wildcards must be opt-in") diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 2447c31719..6345f2c40e 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -667,7 +667,8 @@ func (s *BillingService) initFallbackPricing() { SupportsCacheBreakdown: false, } - // xAI Grok 4.5: $2 input / $0.30 cached input / $6 output below 200k. + // xAI Grok 4.5: $2 input / $0.30 cached input / $6 output below 200k; + // long-context rates are $4 / $0.60 / $12 (>=200k prompt tokens). s.fallbackPrices["grok-4.5"] = &ModelPricing{ InputPricePerToken: 2e-6, OutputPricePerToken: 6e-6, @@ -679,9 +680,8 @@ func (s *BillingService) initFallbackPricing() { LongContextOutputMultiplier: 2, } - // 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). + // xAI Grok 4.6: $2 input / $0.50 cached input / $6 output below 200k; + // long-context rates are $4 / $1 / $12 (>=200k prompt tokens). s.fallbackPrices["grok-4.6"] = &ModelPricing{ InputPricePerToken: 2e-6, OutputPricePerToken: 6e-6, @@ -693,7 +693,8 @@ func (s *BillingService) initFallbackPricing() { LongContextOutputMultiplier: 2, } - // xAI Grok 4.3: $1.25 input / $0.20 cached / $2.50 output below 200k. + // xAI Grok 4.3: $1.25 input / $0.20 cached / $2.50 output below 200k; + // long-context rates are $2.50 / $0.40 / $5. s.fallbackPrices["grok-4.3"] = &ModelPricing{ InputPricePerToken: 1.25e-6, OutputPricePerToken: 2.5e-6, @@ -704,6 +705,18 @@ func (s *BillingService) initFallbackPricing() { LongContextInputMultiplier: 2, LongContextOutputMultiplier: 2, } + // Grok 4.20 variants share the official $1.25 / $0.20 / $2.50 card + // (and $2.50 / $0.40 / $5 long-context rates) with Grok 4.3. + s.fallbackPrices["grok-4.20"] = &ModelPricing{ + InputPricePerToken: 1.25e-6, + OutputPricePerToken: 2.5e-6, + CacheReadPricePerToken: 0.2e-6, + SupportsCacheBreakdown: false, + LongContextInputThreshold: 200000, + LongContextThresholdInclusive: true, + LongContextInputMultiplier: 2, + LongContextOutputMultiplier: 2, + } // Keep legacy Grok 3 Mini requests on their own historical xAI price card; // otherwise the generic Grok fallback bills them as Grok 4.5. @@ -924,21 +937,22 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { } switch modelLower { - case "grok", "grok-latest", "grok-4.5", "grok-4.5-latest": - return s.fallbackPrices["grok-4.5"] - case "grok-4.6", "grok-4.6-latest": + case "grok", "grok-latest", "grok-4.6", "grok-4.6-latest": return s.fallbackPrices["grok-4.6"] + case "grok-4.5", "grok-4.5-latest": + return s.fallbackPrices["grok-4.5"] case "grok-3-mini": return s.fallbackPrices["grok-3-mini"] case "grok-3-mini-fast": return s.fallbackPrices["grok-3-mini-fast"] - case "grok-4.3", - "grok-4.20-0309-reasoning", + case "grok-4.3": + return s.fallbackPrices["grok-4.3"] + case "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-4.20-multi-agent-0309", "grok-4.20-reasoning", "grok-4.20-non-reasoning": - return s.fallbackPrices["grok-4.3"] + return s.fallbackPrices["grok-4.20"] case "grok-build", "grok-build-latest", "grok-build-0.1", "grok-composer", "grok-composer-2.5-fast", "composer-2.5": return s.fallbackPrices["grok-build-0.1"] } @@ -956,7 +970,7 @@ func (s *BillingService) grokUnknownTextFamilyFallback(model string) *ModelPrici if s == nil || !isGrokUnknownTextFamilyModel(model) { return nil } - return s.fallbackPrices["grok-4.5"] + return s.fallbackPrices["grok-4.6"] } func isGrokUnknownTextFamilyModel(model string) bool { diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 1e801d8e95..7248878a2e 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -1179,7 +1179,7 @@ func TestCalculateCostWithLongContext_PropagatesError(t *testing.T) { func TestGetModelPricing_Grok45OfficialFallback(t *testing.T) { svc := newTestBillingService() - for _, model := range []string{"grok", "grok-latest", "grok-4.5", "grok-4.5-latest"} { + for _, model := range []string{"grok-4.5", "grok-4.5-latest"} { model := model t.Run(model, func(t *testing.T) { pricing, err := svc.GetModelPricing(model) @@ -1192,6 +1192,17 @@ func TestGetModelPricing_Grok45OfficialFallback(t *testing.T) { } } +func TestGetModelPricing_GrokBareAliasesUseGrok46(t *testing.T) { + svc := newTestBillingService() + for _, model := range []string{"grok", "grok-latest"} { + pricing, err := svc.GetModelPricing(model) + require.NoError(t, err) + require.InDelta(t, 2e-6, pricing.InputPricePerToken, 1e-12) + require.InDelta(t, 0.5e-6, pricing.CacheReadPricePerToken, 1e-12) + require.InDelta(t, 6e-6, pricing.OutputPricePerToken, 1e-12) + } +} + func TestGetModelPricing_Grok46OfficialFallback(t *testing.T) { svc := newTestBillingService() @@ -1211,6 +1222,25 @@ func TestGetModelPricing_Grok46OfficialFallback(t *testing.T) { } } +func TestGetModelPricing_GrokOfficialFamilyCards(t *testing.T) { + svc := newTestBillingService() + for _, tc := range []struct { + model string + input, cached, output float64 + }{ + {"grok-4.3", 1.25e-6, 0.2e-6, 2.5e-6}, + {"grok-4.20-0309-reasoning", 1.25e-6, 0.2e-6, 2.5e-6}, + {"grok-build-0.1", 1e-6, 0.2e-6, 2e-6}, + } { + p, err := svc.GetModelPricing(tc.model) + require.NoError(t, err, tc.model) + require.InDelta(t, tc.input, p.InputPricePerToken, 1e-12) + require.InDelta(t, tc.cached, p.CacheReadPricePerToken, 1e-12) + require.InDelta(t, tc.output, p.OutputPricePerToken, 1e-12) + require.Equal(t, 200000, p.LongContextInputThreshold) + } +} + func TestCalculateCostUnified_GroupLongContextToggleUsesPresetLadder(t *testing.T) { svc := newTestBillingService() resolver := NewModelPricingResolver(nil, svc) @@ -1234,9 +1264,9 @@ func TestCalculateCostUnified_GroupLongContextToggleUsesPresetLadder(t *testing. require.InDelta(t, disabled.OutputCost*2, enabled.OutputCost, 1e-12) } -func TestGetModelPricing_UnknownGrokTextFallsBackToGrok45(t *testing.T) { +func TestGetModelPricing_UnknownGrokTextFallsBackToGrok46(t *testing.T) { svc := newTestBillingService() - baseline, err := svc.GetModelPricing("grok-4.5") + baseline, err := svc.GetModelPricing("grok-4.6") require.NoError(t, err) for _, model := range []string{"grok-5", "grok-5-latest", "x-ai/grok-7", "grok-4.7-beta"} { diff --git a/backend/internal/service/setting_parse.go b/backend/internal/service/setting_parse.go index 9f3dfbddf2..7787e8810f 100644 --- a/backend/internal/service/setting_parse.go +++ b/backend/internal/service/setting_parse.go @@ -193,7 +193,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeyChannelMonitorShowQuota: "false", // Grok: safe defaults — no cross-vendor model rewrite unless operators enable it. - SettingKeyGrokDefaultTextModel: "grok-4.5", + SettingKeyGrokDefaultTextModel: "grok-4.6", SettingKeyGrokCrossClientModelMapEnabled: "true", SettingKeyGrokDefaultBaseURLMode: GrokDefaultBaseURLModeCLI, @@ -806,7 +806,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin // Grok default mapping policy result.GrokDefaultTextModel = strings.TrimSpace(settings[SettingKeyGrokDefaultTextModel]) if result.GrokDefaultTextModel == "" { - result.GrokDefaultTextModel = "grok-4.5" + result.GrokDefaultTextModel = "grok-4.6" } // Default true (missing/empty → enabled) so Claude/Codex→Grok mapping keeps working. // Operators can set false to disable silent cross-client rewrite. diff --git a/backend/internal/service/setting_update.go b/backend/internal/service/setting_update.go index a4fadded0c..65e28ff6ee 100644 --- a/backend/internal/service/setting_update.go +++ b/backend/internal/service/setting_update.go @@ -423,7 +423,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting if v := strings.TrimSpace(settings.GrokDefaultTextModel); v != "" { updates[SettingKeyGrokDefaultTextModel] = v } else { - updates[SettingKeyGrokDefaultTextModel] = "grok-4.5" + updates[SettingKeyGrokDefaultTextModel] = "grok-4.6" } updates[SettingKeyGrokCrossClientModelMapEnabled] = strconv.FormatBool(settings.GrokCrossClientModelMapEnabled) updates[SettingKeyGrokDefaultBaseURLMode] = normalizeGrokDefaultBaseURLMode(settings.GrokDefaultBaseURLMode) diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index 9fd93e6573..af6cc61820 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -51,7 +51,8 @@ describe('useModelWhitelist', () => { expect(models).toContain('grok-4.5') expect(models).toContain('grok-4.5-latest') expect(models).toContain('grok-build-latest') - expect(models).toContain('grok-imagine-video-1.5-preview') + expect(models).toContain('grok-imagine-image-2.0') + expect(models).toContain('grok-imagine-video-1.5') }) it('combined 模式支持 Grok 4.5 官方别名映射', () => { diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 2ca860c673..4b16d44b02 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -157,8 +157,8 @@ const xaiModels = [ 'grok-imagine', 'grok-imagine-image-quality', 'grok-imagine-image', + 'grok-imagine-image-2.0', 'grok-imagine-video', - 'grok-imagine-video-1.5-preview', 'grok-imagine-video-1.5' ]