更新 Grok 默认模型与官方计费目录

This commit is contained in:
IanShaw
2026-08-20 02:32:22 -07:00
parent ed4207a16f
commit 39485f2e28
10 changed files with 87 additions and 36 deletions
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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
+11 -5
View File
@@ -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"))
}
+4 -4
View File
@@ -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")
+26 -12
View File
@@ -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 {
@@ -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"} {
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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)
@@ -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 官方别名映射', () => {
@@ -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'
]