Merge pull request #3847 from heathermhuang/codex/grok-45-official-support

Add official Grok 4.5 support
This commit is contained in:
Wesley Liddick
2026-07-09 16:07:42 +08:00
committed by GitHub
11 changed files with 137 additions and 18 deletions
@@ -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"))
})
+6 -3
View File
@@ -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"
+5 -2
View File
@@ -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"])
+11 -1
View File
@@ -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"]
@@ -1023,6 +1023,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{},
@@ -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
@@ -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()
@@ -561,9 +605,9 @@ func TestForwardAsChatCompletionsForGrokUsesXAIChatCompletionsAndSnapshots(t *te
require.NoError(t, err)
require.Equal(t, xai.DefaultCLIBaseURL+"/chat/completions", upstream.lastReq.URL.String())
require.Equal(t, "Bearer access-token", upstream.lastReq.Header.Get("Authorization"))
require.Equal(t, "grok-4.3", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "grok-4.5", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "grok", result.Model)
require.Equal(t, "grok-4.3", result.UpstreamModel)
require.Equal(t, "grok-4.5", result.UpstreamModel)
require.Equal(t, 1, result.Usage.InputTokens)
require.Equal(t, 2, result.Usage.OutputTokens)
require.NotNil(t, repo.updates[51][grokQuotaSnapshotExtraKey])
@@ -626,7 +670,7 @@ func TestForwardGrokResponsesStreamingUsesXAIResponsesAndSnapshots(t *testing.T)
require.Equal(t, xai.DefaultCLIBaseURL+"/responses", upstream.lastReq.URL.String())
require.Equal(t, "Bearer access-token", upstream.lastReq.Header.Get("Authorization"))
require.Equal(t, "responses=experimental", upstream.lastReq.Header.Get("OpenAI-Beta"))
require.Equal(t, "grok-4.3", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "grok-4.5", gjson.GetBytes(upstream.lastBody, "model").String())
require.True(t, gjson.GetBytes(upstream.lastBody, "stream").Bool())
require.True(t, result.Stream)
require.Equal(t, "resp_grok", result.ResponseID)
@@ -696,7 +740,7 @@ func TestForwardAsChatCompletionsForGrokStreamingUsesRawXAIChatCompletions(t *te
require.Equal(t, "Bearer access-token", upstream.lastReq.Header.Get("Authorization"))
require.Equal(t, "text/event-stream", upstream.lastReq.Header.Get("Accept"))
require.Equal(t, "sub2api-grok/1.0", upstream.lastReq.Header.Get("User-Agent"))
require.Equal(t, "grok-4.3", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "grok-4.5", gjson.GetBytes(upstream.lastBody, "model").String())
require.True(t, gjson.GetBytes(upstream.lastBody, "stream_options.include_usage").Bool())
require.True(t, result.Stream)
require.Equal(t, 6, result.Usage.InputTokens)
@@ -813,11 +857,11 @@ func TestForwardAsAnthropicForGrokUsesXAIResponses(t *testing.T) {
require.Equal(t, xai.DefaultCLIBaseURL+"/responses", upstream.lastReq.URL.String())
require.Equal(t, "Bearer access-token", upstream.lastReq.Header.Get("Authorization"))
require.Equal(t, "sub2api-grok/1.0", upstream.lastReq.Header.Get("User-Agent"))
require.Equal(t, "grok-4.3", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "grok-4.5", gjson.GetBytes(upstream.lastBody, "model").String())
require.True(t, gjson.GetBytes(upstream.lastBody, "stream").Bool())
require.NotContains(t, string(upstream.lastBody), "chatgpt.com")
require.Equal(t, "grok", result.Model)
require.Equal(t, "grok-4.3", result.UpstreamModel)
require.Equal(t, "grok-4.5", result.UpstreamModel)
require.Equal(t, 5, result.Usage.InputTokens)
require.Equal(t, 2, result.Usage.OutputTokens)
require.Contains(t, recorder.Body.String(), `"type":"message"`)
@@ -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"))
}
@@ -283,7 +283,7 @@ func TestProxyResponsesWebSocketFromClientForGrokUsesXAIHTTPBridge(t *testing.T)
require.Equal(t, xai.DefaultCLIBaseURL+"/responses", upstream.lastReq.URL.String())
require.Equal(t, "Bearer access-token", upstream.lastReq.Header.Get("Authorization"))
require.Equal(t, "sub2api-grok/1.0", upstream.lastReq.Header.Get("User-Agent"))
require.Equal(t, "grok-4.3", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "grok-4.5", gjson.GetBytes(upstream.lastBody, "model").String())
require.False(t, gjson.GetBytes(upstream.lastBody, "type").Exists())
require.False(t, gjson.GetBytes(upstream.lastBody, "generate").Exists())
require.False(t, gjson.GetBytes(upstream.lastBody, "prompt_cache_retention").Exists())
@@ -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')
@@ -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' },