diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go
index 198055f20e..dec30c3bd2 100644
--- a/backend/internal/service/openai_gateway_grok.go
+++ b/backend/internal/service/openai_gateway_grok.go
@@ -52,18 +52,21 @@ func (s *OpenAIGatewayService) forwardGrokResponses(
if isGrokImageGenerationModel(upstreamModel) {
return nil, fmt.Errorf("model %s is an image model and is not available on the Responses endpoint; use /v1/images/generations instead", upstreamModel)
}
- cacheIdentity := resolveGrokCacheIdentity(c, body, "", upstreamModel)
patchedBody, err := patchGrokResponsesBody(body, upstreamModel)
if err != nil {
return nil, err
}
+ // Derive the identity from the request xAI will actually see. This makes
+ // Codex Responses Lite additional_tools part of the stable tool prefix.
+ cacheIdentity := resolveGrokCacheIdentity(c, patchedBody, "", upstreamModel)
+ mixedCacheIntentBody := append([]byte(nil), patchedBody...)
patchedBody, err = applyGrokResponsesCacheIdentity(patchedBody, body, cacheIdentity, account.IsGrokOAuth())
if err != nil {
return nil, fmt.Errorf("apply grok prompt cache identity: %w", err)
}
// Free OAuth + client function tools: reuse Messages mixed-tools cache route
// (append web_search/x_search so xAI does not force non-cacheable build-free).
- patchedBody, err = applyGrokFreeMessagesFunctionToolCacheRoute(patchedBody, body, account, cacheIdentity)
+ patchedBody, err = applyGrokFreeRequestToolCacheRoute(c, patchedBody, mixedCacheIntentBody, account, cacheIdentity)
if err != nil {
return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", err)
}
@@ -374,10 +377,9 @@ func deleteJSONFields(value any, fields map[string]struct{}) bool {
}
// additional_tools is a Codex/Responses Lite private input carrier. xAI's
-// Responses schema accepts ordinary message/function-call input items but
-// rejects this carrier before inference with a ModelInput deserialization
-// error. Top-level supported tools remain available through the separate
-// sanitizeGrokResponsesTools path.
+// Responses schema rejects the carrier itself, but accepts supported tools at
+// the top level. Preserve top-level order, append newly discovered tools in
+// carrier order, then let sanitizeGrokResponsesTools filter unsupported types.
func sanitizeGrokResponsesInput(body []byte) ([]byte, error) {
if !bytes.Contains(body, []byte(`"additional_tools"`)) {
return body, nil
@@ -389,8 +391,36 @@ func sanitizeGrokResponsesInput(body []byte) ([]byte, error) {
rawItems := input.Array()
filtered := make([]json.RawMessage, 0, len(rawItems))
+ topLevelTools := gjson.GetBytes(body, "tools")
+ mergedTools := make([]json.RawMessage, 0)
+ seenTools := make(map[string]struct{})
+ appendTool := func(tool gjson.Result) bool {
+ key := grokResponsesToolDedupKey(tool)
+ if _, exists := seenTools[key]; exists {
+ return false
+ }
+ seenTools[key] = struct{}{}
+ mergedTools = append(mergedTools, json.RawMessage(tool.Raw))
+ return true
+ }
+ if topLevelTools.IsArray() {
+ for _, tool := range topLevelTools.Array() {
+ seenTools[grokResponsesToolDedupKey(tool)] = struct{}{}
+ mergedTools = append(mergedTools, json.RawMessage(tool.Raw))
+ }
+ }
+
+ promoted := false
for _, item := range rawItems {
if strings.TrimSpace(item.Get("type").String()) == "additional_tools" {
+ tools := item.Get("tools")
+ if tools.IsArray() {
+ for _, tool := range tools.Array() {
+ if appendTool(tool) {
+ promoted = true
+ }
+ }
+ }
continue
}
filtered = append(filtered, json.RawMessage(item.Raw))
@@ -402,7 +432,30 @@ func sanitizeGrokResponsesInput(body []byte) ([]byte, error) {
if err != nil {
return nil, err
}
- return sjson.SetRawBytes(body, "input", encoded)
+ body, err = sjson.SetRawBytes(body, "input", encoded)
+ if err != nil || !promoted {
+ return body, err
+ }
+ encodedTools, err := json.Marshal(mergedTools)
+ if err != nil {
+ return nil, err
+ }
+ return sjson.SetRawBytes(body, "tools", encodedTools)
+}
+
+func grokResponsesToolDedupKey(tool gjson.Result) string {
+ toolType := strings.TrimSpace(tool.Get("type").String())
+ if toolType != "" {
+ if name := strings.TrimSpace(tool.Get("name").String()); name != "" {
+ return "type:" + toolType + "\x00name:" + name
+ }
+ if toolType == "mcp" {
+ if label := strings.TrimSpace(tool.Get("server_label").String()); label != "" {
+ return "type:mcp\x00server_label:" + label
+ }
+ }
+ }
+ return "json:" + normalizeCompatSeedJSON(json.RawMessage(tool.Raw))
}
// sanitizeGrokReasoningNullContent 删除 reasoning 项中的 "content": null。
diff --git a/backend/internal/service/openai_gateway_grok_cache.go b/backend/internal/service/openai_gateway_grok_cache.go
index d9e34cbb58..9fb916cacd 100644
--- a/backend/internal/service/openai_gateway_grok_cache.go
+++ b/backend/internal/service/openai_gateway_grok_cache.go
@@ -12,10 +12,12 @@ import (
)
const (
- grokConversationIDHeader = "X-Grok-Conv-Id"
- grokFreeCacheNativeToolsJSON = `[{"type":"web_search"},{"type":"x_search"}]`
- grokFreeCacheDisabledToolChoice = "none"
- grokFreeRolling24hTokenLimit = int64(2_000_000)
+ grokConversationIDHeader = "X-Grok-Conv-Id"
+ grokClientToolCacheOptInHeader = "X-Sub2API-Grok-Client-Tool-Cache"
+ grokFreeCacheNativeToolsJSON = `[{"type":"web_search"},{"type":"x_search"}]`
+ grokFreeCacheDisabledToolChoice = "none"
+ grokClientToolCacheOptInExtraKey = "grok_client_tool_cache_enabled"
+ grokFreeRolling24hTokenLimit = int64(2_000_000)
)
// resolveGrokCacheIdentity derives one stable, tenant-isolated routing identity
@@ -122,7 +124,7 @@ func applyGrokResponsesCacheIdentity(body, intentSourceBody []byte, identity str
// Inspect the pre-sanitization source. patchGrokResponsesBody may remove an
// unsupported client tool and its tool_choice; that must not turn an
// explicit client tool intent into an eligible native-tool request.
- if gjson.GetBytes(intentSourceBody, "tools").Exists() || gjson.GetBytes(intentSourceBody, "tool_choice").Exists() {
+ if hasGrokResponsesToolIntent(intentSourceBody) {
return out, nil
}
out, err = sjson.SetRawBytes(out, "tools", []byte(grokFreeCacheNativeToolsJSON))
@@ -132,12 +134,113 @@ func applyGrokResponsesCacheIdentity(body, intentSourceBody []byte, identity str
return sjson.SetBytes(out, "tool_choice", grokFreeCacheDisabledToolChoice)
}
+func hasGrokResponsesToolIntent(body []byte) bool {
+ if gjson.GetBytes(body, "tools").Exists() || gjson.GetBytes(body, "tool_choice").Exists() {
+ return true
+ }
+ input := gjson.GetBytes(body, "input")
+ if !input.IsArray() {
+ return false
+ }
+ for _, item := range input.Array() {
+ if strings.TrimSpace(item.Get("type").String()) != "additional_tools" {
+ continue
+ }
+ tools := item.Get("tools")
+ if !tools.Exists() || !tools.IsArray() || len(tools.Array()) > 0 {
+ return true
+ }
+ }
+ return false
+}
+
// applyGrokFreeMessagesFunctionToolCacheRoute enables xAI's cache-capable
-// mixed-tools route only for the Anthropic Messages bridge and only when the
-// selected account is known to be Free. Native tools become eligible under
-// auto selection, so callers must not apply this policy to paid accounts or
-// other ingress protocols implicitly.
+// mixed-tools route only for known Free accounts. Pure client tools default to
+// the cache-capable route so an intermediate sub2api does not need to preserve
+// client-specific opt-in headers. Operators can explicitly disable this per
+// account when native search tools would change the desired behavior (#4486).
func applyGrokFreeMessagesFunctionToolCacheRoute(body, intentSourceBody []byte, account *Account, cacheIdentity string) ([]byte, error) {
+ allowPureClientTools, _ := grokClientToolCacheAccountPolicy(account)
+ return applyGrokFreeToolCacheRoute(body, intentSourceBody, account, cacheIdentity, allowPureClientTools, true)
+}
+
+// applyGrokFreeRequestToolCacheRoute also accepts a request-scoped opt-in. The
+// sub2api header is consumed locally because buildGrokResponsesRequest only
+// forwards the explicitly supported OpenAI-Beta header from downstream.
+func applyGrokFreeRequestToolCacheRoute(c *gin.Context, body, intentSourceBody []byte, account *Account, cacheIdentity string) ([]byte, error) {
+ allowPureClientTools, accountPolicyExplicit := grokClientToolCacheAccountPolicy(account)
+ requestOptOut := false
+ if c != nil {
+ switch strings.ToLower(strings.TrimSpace(c.GetHeader(grokClientToolCacheOptInHeader))) {
+ case "1", "true", "yes", "on", "prefer-cache":
+ allowPureClientTools = true
+ case "0", "false", "no", "off":
+ allowPureClientTools = false
+ requestOptOut = true
+ }
+ }
+ if !allowPureClientTools && !accountPolicyExplicit && !requestOptOut && isGrokClaudeDesktopResponsesCacheRequest(c) {
+ allowPureClientTools = true
+ }
+ // A function merely named web_search/x_search is still a client function.
+ // Known Free OAuth accounts use the cache route by default; a request-scoped
+ // opt-in may override an account opt-out, while an explicit request opt-out
+ // always wins. The legacy Claude fingerprint remains only as a compatibility
+ // fallback when no account policy has been recorded (#4486).
+ return applyGrokFreeToolCacheRoute(body, intentSourceBody, account, cacheIdentity, allowPureClientTools, allowPureClientTools)
+}
+
+// grokClientToolCacheAccountPolicy is intentionally strict for configured
+// values: only a JSON boolean is accepted. A missing key defaults on solely for
+// accounts positively identified as Grok Free OAuth; paid, API-key, and unknown
+// accounts remain fail-closed.
+func grokClientToolCacheAccountPolicy(account *Account) (enabled, explicit bool) {
+ if !isKnownGrokFreeAccount(account) {
+ return false, false
+ }
+ if account.Extra == nil {
+ return true, false
+ }
+ value, exists := account.Extra[grokClientToolCacheOptInExtraKey]
+ if !exists {
+ return true, false
+ }
+ enabled, valid := value.(bool)
+ if !valid {
+ return false, true
+ }
+ return enabled, true
+}
+
+// isGrokClaudeDesktopResponsesCacheRequest recognizes the strict wire
+// fingerprint emitted when Claude Desktop's local agent is translated by
+// CC Switch into an OpenAI Responses request. Requiring every independent
+// signal prevents a generic Claude-compatible client (or the Chat bridge)
+// from silently opting into the mixed native/client tool route.
+func isGrokClaudeDesktopResponsesCacheRequest(c *gin.Context) bool {
+ if c == nil || c.Request == nil || c.Request.URL == nil || isOpenAIResponsesCompactPath(c) {
+ return false
+ }
+ path := strings.TrimRight(strings.TrimSpace(c.Request.URL.Path), "/")
+ if !strings.HasSuffix(path, "/responses") {
+ return false
+ }
+
+ if !claudeCodeUAPattern.MatchString(strings.TrimSpace(c.GetHeader("User-Agent"))) {
+ return false
+ }
+ switch strings.ToLower(strings.TrimSpace(c.GetHeader("X-App"))) {
+ case "cli", "cli-bg":
+ default:
+ return false
+ }
+ if !strings.EqualFold(strings.TrimSpace(c.GetHeader("anthropic-client-platform")), "desktop_app") {
+ return false
+ }
+ return strings.TrimSpace(c.GetHeader("X-Claude-Code-Session-Id")) != ""
+}
+
+func applyGrokFreeToolCacheRoute(body, intentSourceBody []byte, account *Account, cacheIdentity string, allowPureClientTools, allowFunctionSearch bool) ([]byte, error) {
if strings.TrimSpace(cacheIdentity) == "" || !isKnownGrokFreeAccount(account) {
return body, nil
}
@@ -146,7 +249,12 @@ func applyGrokFreeMessagesFunctionToolCacheRoute(body, intentSourceBody []byte,
if !isGrokFreeCacheFunctionToolIntent(intentTools, intentToolChoice) {
return body, nil
}
- return appendMissingGrokFreeCacheNativeTools(body)
+ if intentToolChoice.Type == gjson.String && strings.TrimSpace(intentToolChoice.String()) == grokFreeCacheDisabledToolChoice {
+ // Adding native cache-routing tools cannot change behavior when the
+ // client has explicitly disabled all tool execution.
+ return appendGrokFreeCacheNativeToolsWithPolicy(body, true, false)
+ }
+ return appendGrokFreeCacheNativeToolsWithPolicy(body, allowPureClientTools, allowFunctionSearch)
}
func isKnownGrokFreeAccount(account *Account) bool {
@@ -231,22 +339,44 @@ func isGrokFreeCacheFunctionToolIntent(tools, toolChoice gjson.Result) bool {
return false
}
for _, tool := range items {
- if !tool.IsObject() || strings.TrimSpace(tool.Get("type").String()) != "function" {
+ if !tool.IsObject() {
return false
}
- // Responses function declarations keep name at the top level. Reject
- // Chat Completions' nested function shape and incomplete declarations.
- if strings.TrimSpace(tool.Get("name").String()) == "" || tool.Get("function").Exists() {
+ toolType := strings.TrimSpace(tool.Get("type").String())
+ if _, ok := grokResponsesSupportedToolTypes[toolType]; !ok {
return false
}
+ if toolType == "function" {
+ // Responses function declarations keep name at the top level. Reject
+ // Chat Completions' nested function shape and incomplete declarations.
+ if strings.TrimSpace(tool.Get("name").String()) == "" || tool.Get("function").Exists() {
+ return false
+ }
+ }
}
if !toolChoice.Exists() {
return true
}
- return toolChoice.Type == gjson.String && strings.TrimSpace(toolChoice.String()) == "auto"
+ if toolChoice.Type != gjson.String {
+ return false
+ }
+ switch strings.TrimSpace(toolChoice.String()) {
+ case "auto", grokFreeCacheDisabledToolChoice:
+ return true
+ default:
+ return false
+ }
}
func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) {
+ return appendGrokFreeCacheNativeTools(body, false)
+}
+
+func appendGrokFreeCacheNativeTools(body []byte, allowPureClientTools bool) ([]byte, error) {
+ return appendGrokFreeCacheNativeToolsWithPolicy(body, allowPureClientTools, true)
+}
+
+func appendGrokFreeCacheNativeToolsWithPolicy(body []byte, allowPureClientTools, allowFunctionSearch bool) ([]byte, error) {
tools := gjson.GetBytes(body, "tools")
if !tools.Exists() || !tools.IsArray() {
return body, nil
@@ -256,9 +386,19 @@ func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) {
if len(items) == 0 {
return body, nil
}
+ hasNativeSearch := false
+ for _, tool := range items {
+ switch strings.TrimSpace(tool.Get("type").String()) {
+ case "web_search", "x_search":
+ hasNativeSearch = true
+ }
+ }
+ if !allowPureClientTools && !allowFunctionSearch && !hasNativeSearch {
+ return body, nil
+ }
merged := make([]json.RawMessage, 0, len(items)+2)
present := make(map[string]bool, 2)
- hasFunction := false
+ hasCompanionTool := false
for _, tool := range items {
toolType := strings.TrimSpace(tool.Get("type").String())
switch toolType {
@@ -269,7 +409,7 @@ func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) {
}
// Grok Build may declare search as function tools. Convert to native
// entries so Free OAuth stays cache-capable without duplicate names.
- if name == "web_search" || name == "x_search" {
+ if (name == "web_search" || name == "x_search") && allowFunctionSearch {
if present[name] {
continue
}
@@ -279,9 +419,17 @@ func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) {
}
merged = append(merged, raw)
present[name] = true
+ if allowPureClientTools {
+ hasCompanionTool = true
+ }
continue
}
- hasFunction = true
+ if name == "web_search" || name == "x_search" {
+ // Keep the client function intact and avoid adding a same-named
+ // native tool unless conversion was explicitly enabled.
+ present[name] = true
+ }
+ hasCompanionTool = true
merged = append(merged, json.RawMessage(tool.Raw))
case "web_search", "x_search":
if present[toolType] {
@@ -290,17 +438,21 @@ func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) {
merged = append(merged, json.RawMessage(tool.Raw))
present[toolType] = true
default:
- return body, nil
+ if _, ok := grokResponsesSupportedToolTypes[toolType]; !ok {
+ return body, nil
+ }
+ hasCompanionTool = true
+ merged = append(merged, json.RawMessage(tool.Raw))
}
}
- if !hasFunction {
+ if !hasCompanionTool {
return body, nil
}
// Only complement missing native search tools when the request already contains
// at least one search tool (native or function-form). Pure client function tools
// (e.g. view_image) must not trigger injection to avoid biasing model tool
// selection (#4486).
- if !present["web_search"] && !present["x_search"] {
+ if !allowPureClientTools && !present["web_search"] && !present["x_search"] {
return body, nil
}
for _, toolType := range []string{"web_search", "x_search"} {
diff --git a/backend/internal/service/openai_gateway_grok_cache_test.go b/backend/internal/service/openai_gateway_grok_cache_test.go
index 73b1ea5ec1..90155aadc6 100644
--- a/backend/internal/service/openai_gateway_grok_cache_test.go
+++ b/backend/internal/service/openai_gateway_grok_cache_test.go
@@ -197,7 +197,7 @@ func TestApplyGrokCacheIdentityWritesResponsesBodyAndHeader(t *testing.T) {
require.False(t, gjson.GetBytes(unscopedBody, "tool_choice").Exists())
}
-func TestApplyGrokCacheIdentityAppendsNativeToolsToResponseFunctions(t *testing.T) {
+func TestGrokFreeMessagesClientToolCacheDefaultsOnForKnownFree(t *testing.T) {
account := healthyGrokOAuthGatewayTestAccount(901, "access-token")
account.Credentials["subscription_tier"] = " FREE "
tests := []struct {
@@ -211,7 +211,6 @@ func TestApplyGrokCacheIdentityAppendsNativeToolsToResponseFunctions(t *testing.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- // Pure client function tools without search → no native injection (#4486).
intentBody := []byte(`{"model":"grok","tools":[{"type":"function","name":"lookup","description":"look up a value","parameters":{"type":"object"}},{"type":"function","name":"save","parameters":{"type":"object"}}]` + tt.toolChoiceJSON + `}`)
body, err := applyGrokResponsesCacheIdentity(intentBody, intentBody, "isolated-id", true)
require.NoError(t, err)
@@ -220,16 +219,46 @@ func TestApplyGrokCacheIdentityAppendsNativeToolsToResponseFunctions(t *testing.
require.NoError(t, err)
require.Equal(t, "isolated-id", gjson.GetBytes(body, "prompt_cache_key").String())
tools := gjson.GetBytes(body, "tools").Array()
- require.Len(t, tools, 2, "pure client functions should not get native search injected")
+ require.Len(t, tools, 4)
require.Equal(t, "function", tools[0].Get("type").String())
require.Equal(t, "lookup", tools[0].Get("name").String())
require.Equal(t, "function", tools[1].Get("type").String())
require.Equal(t, "save", tools[1].Get("name").String())
+ require.Equal(t, "web_search", tools[2].Get("type").String())
+ require.Equal(t, "x_search", tools[3].Get("type").String())
require.Equal(t, tt.wantChoice, gjson.GetBytes(body, "tool_choice").Exists())
})
}
}
+func TestGrokFreeMessagesClientToolCacheDefaultsWithMissingAccountSetting(t *testing.T) {
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+ tests := []struct {
+ name string
+ extra map[string]any
+ }{
+ {name: "nil extra"},
+ {name: "empty extra", extra: map[string]any{}},
+ {name: "unrelated extra", extra: map[string]any{"other_setting": true}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90101, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ account.Extra = tt.extra
+
+ patched, err := applyGrokFreeMessagesFunctionToolCacheRoute(body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ tools := gjson.GetBytes(patched, "tools").Array()
+ require.Len(t, tools, 3)
+ require.Equal(t, "web_search", tools[1].Get("type").String())
+ require.Equal(t, "x_search", tools[2].Get("type").String())
+ })
+ }
+}
+
func TestApplyGrokCacheIdentityAppendsNativeToolsWhenSearchPresent(t *testing.T) {
account := healthyGrokOAuthGatewayTestAccount(901, "access-token")
account.Credentials["subscription_tier"] = " FREE "
@@ -249,6 +278,330 @@ func TestApplyGrokCacheIdentityAppendsNativeToolsWhenSearchPresent(t *testing.T)
require.Equal(t, "x_search", tools[2].Get("type").String())
}
+func TestGrokFreeClientToolCacheAccountOptIn(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(9011, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ account.Extra = map[string]any{grokClientToolCacheOptInExtraKey: true}
+ intentBody := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}},{"type":"function","name":"read_file","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ body, err := applyGrokResponsesCacheIdentity(intentBody, intentBody, "isolated-id", true)
+ require.NoError(t, err)
+ body, err = applyGrokFreeMessagesFunctionToolCacheRoute(body, intentBody, account, "isolated-id")
+ require.NoError(t, err)
+
+ tools := gjson.GetBytes(body, "tools").Array()
+ require.Len(t, tools, 4)
+ require.Equal(t, "view_image", tools[0].Get("name").String())
+ require.Equal(t, "read_file", tools[1].Get("name").String())
+ require.Equal(t, "web_search", tools[2].Get("type").String())
+ require.Equal(t, "x_search", tools[3].Get("type").String())
+}
+
+func TestGrokFreeMessagesClientToolCacheAccountOptOut(t *testing.T) {
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+ tests := []struct {
+ name string
+ value any
+ }{
+ {name: "explicit false", value: false},
+ {name: "string false is malformed", value: "false"},
+ {name: "numeric value is malformed", value: 1},
+ {name: "null value is malformed", value: nil},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90111, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ account.Extra = map[string]any{grokClientToolCacheOptInExtraKey: tt.value}
+
+ patched, err := applyGrokFreeMessagesFunctionToolCacheRoute(body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ require.JSONEq(t, string(body), string(patched))
+ })
+ }
+}
+
+func TestGrokFreeClientToolCacheRequestOptInOverridesAccountOptOut(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(9014, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ account.Extra = map[string]any{grokClientToolCacheOptInExtraKey: false}
+ c := newGrokCacheTestContext(9014)
+ c.Request.Header.Set(grokClientToolCacheOptInHeader, "prefer-cache")
+ intentBody := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ body, err := applyGrokResponsesCacheIdentity(intentBody, intentBody, "isolated-id", true)
+ require.NoError(t, err)
+ body, err = applyGrokFreeRequestToolCacheRoute(c, body, intentBody, account, "isolated-id")
+ require.NoError(t, err)
+
+ tools := gjson.GetBytes(body, "tools").Array()
+ require.Len(t, tools, 3)
+ require.Equal(t, "view_image", tools[0].Get("name").String())
+ require.Equal(t, "web_search", tools[1].Get("type").String())
+ require.Equal(t, "x_search", tools[2].Get("type").String())
+}
+
+func TestGrokFreeChatRequestClientToolCacheDefaultsOnWithoutClientFingerprint(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90140, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ c := newGrokCacheTestContext(90140)
+ c.Request.URL.Path = "/v1/chat/completions"
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ tools := gjson.GetBytes(patched, "tools").Array()
+ require.Len(t, tools, 3)
+ require.Equal(t, "view_image", tools[0].Get("name").String())
+ require.Equal(t, "web_search", tools[1].Get("type").String())
+ require.Equal(t, "x_search", tools[2].Get("type").String())
+}
+
+func TestGrokFreeClientToolCacheClaudeDesktopResponsesAutoOptIn(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90141, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"Read","parameters":{"type":"object"}},{"type":"function","name":"Edit","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ for _, xApp := range []string{"cli", "cli-bg"} {
+ t.Run(xApp, func(t *testing.T) {
+ c := newGrokCacheTestContext(90141)
+ // The desktop marker text is intentionally not required; CC Switch and
+ // Claude Desktop may change the descriptive User-Agent suffix.
+ c.Request.Header.Set("User-Agent", "claude-cli/2.1.215 (external, future-desktop, agent-sdk/0.3.215)")
+ c.Request.Header.Set("X-App", xApp)
+ c.Request.Header.Set("anthropic-client-platform", "desktop_app")
+ c.Request.Header.Set("X-Claude-Code-Session-Id", "desktop-session-1")
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ tools := gjson.GetBytes(patched, "tools").Array()
+ require.Len(t, tools, 4)
+ require.Equal(t, "Read", tools[0].Get("name").String())
+ require.Equal(t, "Edit", tools[1].Get("name").String())
+ require.Equal(t, "web_search", tools[2].Get("type").String())
+ require.Equal(t, "x_search", tools[3].Get("type").String())
+ })
+ }
+}
+
+func TestGrokFreeClientToolCacheClaudeDesktopFingerprintRequiresAllSignals(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90142, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ account.Extra = map[string]any{grokClientToolCacheOptInExtraKey: false}
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"Read","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ tests := []struct {
+ name string
+ path string
+ ua string
+ xApp string
+ platform string
+ session string
+ }{
+ {
+ name: "chat path",
+ path: "/v1/chat/completions",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ xApp: "cli",
+ platform: "desktop_app",
+ session: "desktop-session-1",
+ },
+ {
+ name: "compact responses path",
+ path: "/v1/responses/compact",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ xApp: "cli",
+ platform: "desktop_app",
+ session: "desktop-session-1",
+ },
+ {
+ name: "non claude cli user agent",
+ path: "/v1/responses",
+ ua: "Mozilla/5.0 (claude-desktop-3p)",
+ xApp: "cli",
+ platform: "desktop_app",
+ session: "desktop-session-1",
+ },
+ {
+ name: "missing x app",
+ path: "/v1/responses",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ platform: "desktop_app",
+ session: "desktop-session-1",
+ },
+ {
+ name: "wrong x app",
+ path: "/v1/responses",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ xApp: "desktop",
+ platform: "desktop_app",
+ session: "desktop-session-1",
+ },
+ {
+ name: "missing client platform",
+ path: "/v1/responses",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ xApp: "cli",
+ session: "desktop-session-1",
+ },
+ {
+ name: "wrong client platform",
+ path: "/v1/responses",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ xApp: "cli",
+ platform: "web",
+ session: "desktop-session-1",
+ },
+ {
+ name: "missing session header",
+ path: "/v1/responses",
+ ua: "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)",
+ xApp: "cli",
+ platform: "desktop_app",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := newGrokCacheTestContext(90142)
+ c.Request.URL.Path = tt.path
+ if tt.ua != "" {
+ c.Request.Header.Set("User-Agent", tt.ua)
+ }
+ if tt.xApp != "" {
+ c.Request.Header.Set("X-App", tt.xApp)
+ }
+ if tt.platform != "" {
+ c.Request.Header.Set("anthropic-client-platform", tt.platform)
+ }
+ if tt.session != "" {
+ c.Request.Header.Set("X-Claude-Code-Session-Id", tt.session)
+ }
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ require.JSONEq(t, string(body), string(patched))
+ })
+ }
+}
+
+func TestGrokFreeClientToolCacheExplicitRequestOptOut(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90144, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"Read","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ for _, value := range []string{"0", "false", "no", "off"} {
+ t.Run(value, func(t *testing.T) {
+ c := newGrokCacheTestContext(90144)
+ c.Request.URL.Path = "/v1/chat/completions"
+ c.Request.Header.Set(grokClientToolCacheOptInHeader, value)
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ require.JSONEq(t, string(body), string(patched))
+ })
+ }
+}
+
+func TestGrokFreeClientToolCacheClaudeDesktopAutoOptInDoesNotOverridePaidTier(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(90143, "access-token")
+ account.Credentials["subscription_tier"] = "supergrok"
+ c := newGrokCacheTestContext(90143)
+ c.Request.Header.Set("User-Agent", "claude-cli/2.1.215 (external, claude-desktop-3p, agent-sdk/0.3.215)")
+ c.Request.Header.Set("X-App", "cli")
+ c.Request.Header.Set("anthropic-client-platform", "desktop_app")
+ c.Request.Header.Set("X-Claude-Code-Session-Id", "desktop-session-1")
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"Read","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ require.JSONEq(t, string(body), string(patched))
+}
+
+func TestGrokFreeRequestClientSearchFunctionUsesDefaultAccountPolicy(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(9015, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ c := newGrokCacheTestContext(9015)
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}},{"type":"function","name":"web_search","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ tools := gjson.GetBytes(patched, "tools").Array()
+ require.Len(t, tools, 3)
+ require.Equal(t, "view_image", tools[0].Get("name").String())
+ require.Equal(t, "web_search", tools[1].Get("type").String())
+ require.Equal(t, "x_search", tools[2].Get("type").String())
+}
+
+func TestGrokFreeRequestToolChoiceNoneUsesSafeCacheRoute(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(9016, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ c := newGrokCacheTestContext(9016)
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}}],"tool_choice":"none"}`)
+
+ patched, err := applyGrokFreeRequestToolCacheRoute(c, body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ tools := gjson.GetBytes(patched, "tools").Array()
+ require.Len(t, tools, 3)
+ require.Equal(t, "view_image", tools[0].Get("name").String())
+ require.Equal(t, "web_search", tools[1].Get("type").String())
+ require.Equal(t, "x_search", tools[2].Get("type").String())
+ require.Equal(t, "none", gjson.GetBytes(patched, "tool_choice").String())
+}
+
+func TestApplyGrokCacheIdentityRecognizesResponsesLiteAdditionalTools(t *testing.T) {
+ intentBody := []byte(`{"model":"grok","input":[{"type":"additional_tools","tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]},{"type":"message","role":"user","content":"hello"}]}`)
+ patchedBody := []byte(`{"model":"grok-4.5","tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}],"input":[{"type":"message","role":"user","content":"hello"}]}`)
+
+ patched, err := applyGrokResponsesCacheIdentity(patchedBody, intentBody, "isolated-id", true)
+
+ require.NoError(t, err)
+ tools := gjson.GetBytes(patched, "tools").Array()
+ require.Len(t, tools, 1)
+ require.Equal(t, "lookup", tools[0].Get("name").String())
+ require.False(t, gjson.GetBytes(patched, "tool_choice").Exists())
+ require.Equal(t, "isolated-id", gjson.GetBytes(patched, "prompt_cache_key").String())
+}
+
+func TestGrokFreeCacheRoutePreservesMixedSupportedToolsWithSearchIntent(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(9012, "access-token")
+ account.Credentials["subscription_tier"] = "free"
+ intentBody := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}},{"type":"shell"},{"type":"web_search"}],"tool_choice":"auto"}`)
+
+ body, err := applyGrokResponsesCacheIdentity(intentBody, intentBody, "isolated-id", true)
+ require.NoError(t, err)
+ body, err = applyGrokFreeMessagesFunctionToolCacheRoute(body, intentBody, account, "isolated-id")
+ require.NoError(t, err)
+
+ tools := gjson.GetBytes(body, "tools").Array()
+ require.Len(t, tools, 4)
+ require.Equal(t, "function", tools[0].Get("type").String())
+ require.Equal(t, "shell", tools[1].Get("type").String())
+ require.Equal(t, "web_search", tools[2].Get("type").String())
+ require.Equal(t, "x_search", tools[3].Get("type").String())
+}
+
+func TestGrokClientToolCacheOptInDoesNotOverridePaidTier(t *testing.T) {
+ account := healthyGrokOAuthGatewayTestAccount(9013, "access-token")
+ account.Credentials["subscription_tier"] = "supergrok"
+ account.Extra = map[string]any{grokClientToolCacheOptInExtraKey: true}
+ body := []byte(`{"model":"grok","tools":[{"type":"function","name":"view_image","parameters":{"type":"object"}}],"tool_choice":"auto"}`)
+
+ patched, err := applyGrokFreeMessagesFunctionToolCacheRoute(body, body, account, "isolated-id")
+
+ require.NoError(t, err)
+ require.JSONEq(t, string(body), string(patched))
+}
+
func TestApplyGrokCacheIdentityRequiresPatchedFunctionTools(t *testing.T) {
account := healthyGrokOAuthGatewayTestAccount(902, "access-token")
account.Credentials["subscription_tier"] = "free"
@@ -260,7 +613,7 @@ func TestApplyGrokCacheIdentityRequiresPatchedFunctionTools(t *testing.T) {
{name: "missing tools", patchedBody: `{"model":"grok-4.5"}`},
{name: "empty tools", patchedBody: `{"model":"grok-4.5","tools":[]}`},
{name: "native tools only", patchedBody: `{"model":"grok-4.5","tools":[{"type":"web_search"}]}`},
- {name: "unexpected patched tool", patchedBody: `{"model":"grok-4.5","tools":[{"type":"function","name":"lookup"},{"type":"mcp","name":"server"}]}`},
+ {name: "unexpected patched tool", patchedBody: `{"model":"grok-4.5","tools":[{"type":"function","name":"lookup"},{"type":"namespace","name":"server"}]}`},
}
for _, tt := range tests {
@@ -280,8 +633,7 @@ func TestApplyGrokCacheIdentityRequiresPatchedFunctionTools(t *testing.T) {
}
func TestGrokFreeMessagesFunctionToolCacheRouteRequiresKnownFreeTier(t *testing.T) {
- // Include web_search as function to trigger native tool injection (pure client
- // functions no longer trigger injection after #4486 fix).
+ // Include web_search as a function to also cover its conversion to a native tool.
intentBody := []byte(`{"model":"grok","tools":[{"type":"function","name":"lookup"},{"type":"function","name":"web_search"}],"tool_choice":"auto"}`)
tests := []struct {
name string
diff --git a/backend/internal/service/openai_gateway_grok_chat_bridge.go b/backend/internal/service/openai_gateway_grok_chat_bridge.go
index 0b8f0e2d83..89ebb4049e 100644
--- a/backend/internal/service/openai_gateway_grok_chat_bridge.go
+++ b/backend/internal/service/openai_gateway_grok_chat_bridge.go
@@ -22,17 +22,23 @@ const (
var grokChatResponsesBridgeTopLevelFields = map[string]struct{}{
"model": {},
"messages": {},
+ "instructions": {},
"stream": {},
"stream_options": {},
"max_tokens": {},
"max_completion_tokens": {},
"temperature": {},
"top_p": {},
+ "stop": {},
+ "reasoning_effort": {},
"prompt_cache_key": {},
"tools": {},
"tool_choice": {},
"functions": {},
"function_call": {},
+ "parallel_tool_calls": {},
+ "response_format": {},
+ "service_tier": {},
}
// grokChatResponsesBridgeEligibility deliberately accepts only request shapes
@@ -45,18 +51,49 @@ func grokChatResponsesBridgeEligibility(body []byte) (bool, string) {
return false, "invalid_json"
}
+ // These fields have no effect when explicitly set to JSON null. Accepting
+ // that common SDK representation keeps the request on the bridge path,
+ // while non-null values remain unsupported because the Responses converter
+ // cannot preserve their Chat Completions semantics.
for _, field := range []string{"stop", "reasoning_effort"} {
- if _, exists := root[field]; exists {
+ if raw, exists := root[field]; exists && !grokChatJSONNull(raw) {
return false, "unsupported_" + field
}
}
- for _, field := range []string{"tools", "functions"} {
- if raw, exists := root[field]; exists && !grokChatNullOrEmptyArray(raw) {
- return false, "unsupported_" + field
+ if raw, exists := root["instructions"]; exists {
+ var instructions string
+ if !grokChatJSONNull(raw) && json.Unmarshal(raw, &instructions) != nil {
+ return false, "invalid_instructions"
}
}
- if raw, exists := root["tool_choice"]; exists && !grokChatNullOrNone(raw) {
- return false, "unsupported_tool_choice"
+ if raw, exists := root["response_format"]; exists {
+ var responseFormat map[string]json.RawMessage
+ if !grokChatJSONNull(raw) && (json.Unmarshal(raw, &responseFormat) != nil || responseFormat == nil) {
+ return false, "invalid_response_format"
+ }
+ }
+ if raw, exists := root["service_tier"]; exists {
+ var serviceTier string
+ if !grokChatJSONNull(raw) && json.Unmarshal(raw, &serviceTier) != nil {
+ return false, "invalid_service_tier"
+ }
+ }
+ if raw, exists := root["tools"]; exists {
+ if ok, reason := grokChatFunctionDeclarationsBridgeable(raw); !ok {
+ return false, reason
+ }
+ }
+ if raw, exists := root["functions"]; exists && !grokChatNullOrEmptyArray(raw) {
+ return false, "unsupported_functions"
+ }
+ if raw, exists := root["tool_choice"]; exists {
+ if ok, reason := grokChatToolChoiceBridgeable(raw); !ok {
+ return false, reason
+ }
+ var choice string
+ if json.Unmarshal(raw, &choice) == nil && choice == "required" && !grokChatHasFunctionDeclarations(root) {
+ return false, "required_tool_choice_without_tools"
+ }
}
if raw, exists := root["function_call"]; exists && !grokChatNullOrNone(raw) {
return false, "unsupported_function_call"
@@ -78,6 +115,12 @@ func grokChatResponsesBridgeEligibility(body []byte) (bool, string) {
return false, "invalid_stream"
}
}
+ if raw, ok := root["parallel_tool_calls"]; ok {
+ var parallelToolCalls *bool
+ if json.Unmarshal(raw, ¶llelToolCalls) != nil || parallelToolCalls == nil {
+ return false, "invalid_parallel_tool_calls"
+ }
+ }
if raw, ok := root["stream_options"]; ok {
var options map[string]json.RawMessage
if json.Unmarshal(raw, &options) != nil || options == nil {
@@ -128,42 +171,256 @@ func grokChatResponsesBridgeEligibility(body []byte) (bool, string) {
return false, "invalid_messages"
}
for _, message := range messages {
- for field := range message {
- if field != "role" && field != "content" {
- return false, "unsafe_message_field_" + field
- }
- }
var role string
if raw, exists := message["role"]; !exists || json.Unmarshal(raw, &role) != nil {
return false, "invalid_message_role"
}
switch role {
- case "system", "user", "assistant":
+ case "system", "user":
+ if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content"); !ok {
+ return false, reason
+ }
+ raw, exists := message["content"]
+ if !exists {
+ return false, "non_text_message_content"
+ }
+ if ok, reason := grokChatRequiredMessageContentBridgeable(raw); !ok {
+ return false, reason
+ }
+ case "assistant":
+ if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content", "reasoning_content", "tool_calls"); !ok {
+ return false, reason
+ }
+ reasoningContent := ""
+ if raw, exists := message["reasoning_content"]; exists {
+ if !grokChatJSONNull(raw) && json.Unmarshal(raw, &reasoningContent) != nil {
+ return false, "invalid_reasoning_content"
+ }
+ }
+ hasReasoningContent := strings.TrimSpace(reasoningContent) != ""
+ toolCallCount := 0
+ if raw, exists := message["tool_calls"]; exists {
+ var reason string
+ toolCallCount, reason = grokChatAssistantToolCallsBridgeable(raw)
+ if reason != "" {
+ return false, reason
+ }
+ }
+ raw, hasContent := message["content"]
+ if !hasContent || strings.TrimSpace(string(raw)) == "null" {
+ if toolCallCount == 0 && !hasReasoningContent {
+ return false, "non_text_message_content"
+ }
+ continue
+ }
+ var content string
+ if json.Unmarshal(raw, &content) == nil {
+ if strings.TrimSpace(content) == "" && toolCallCount == 0 && !hasReasoningContent {
+ return false, "empty_message_content"
+ }
+ continue
+ }
+ if ok, reason := grokChatStructuredContentBridgeable(raw); !ok {
+ // The converter can still emit a standalone reasoning part when
+ // an otherwise empty content array accompanies reasoning_content.
+ // Do not broaden this exception to unsupported/malformed parts.
+ if !hasReasoningContent || reason != "empty_message_content" {
+ return false, reason
+ }
+ }
+ case "tool":
+ if ok, reason := grokChatMessageFieldsBridgeable(message, "role", "content", "tool_call_id"); !ok {
+ return false, reason
+ }
+ var callID string
+ if raw, exists := message["tool_call_id"]; !exists || json.Unmarshal(raw, &callID) != nil || strings.TrimSpace(callID) == "" {
+ return false, "invalid_tool_call_id"
+ }
+ var output string
+ if raw, exists := message["content"]; !exists || json.Unmarshal(raw, &output) != nil || output == "" {
+ return false, "invalid_tool_message_content"
+ }
default:
return false, "unsupported_message_role_" + role
}
- raw, exists := message["content"]
- if !exists {
- return false, "non_text_message_content"
- }
- var content string
- if json.Unmarshal(raw, &content) == nil {
- if strings.TrimSpace(content) == "" {
- return false, "empty_message_content"
- }
- continue
- }
- // Structured content: only allow arrays whose parts are text or
- // image_url. These are losslessly convertible to Responses input_text/
- // input_image parts, so the bridge preserves Chat Completions semantics.
- if ok, reason := grokChatStructuredContentBridgeable(raw); !ok {
- return false, reason
- }
}
return true, ""
}
+func grokChatFunctionDeclarationsBridgeable(raw json.RawMessage) (bool, string) {
+ if strings.TrimSpace(string(raw)) == "null" {
+ return true, ""
+ }
+ var declarations []json.RawMessage
+ if json.Unmarshal(raw, &declarations) != nil {
+ return false, "invalid_tools"
+ }
+ for _, declaration := range declarations {
+ var tool map[string]json.RawMessage
+ if json.Unmarshal(declaration, &tool) != nil || tool == nil {
+ return false, "invalid_tool"
+ }
+ for field := range tool {
+ if field != "type" && field != "function" {
+ return false, "unsafe_tool_field_" + field
+ }
+ }
+ var toolType string
+ if rawType, exists := tool["type"]; !exists || json.Unmarshal(rawType, &toolType) != nil || toolType != "function" {
+ return false, "unsupported_tool_type"
+ }
+ functionRaw, exists := tool["function"]
+ if !exists {
+ return false, "invalid_tool_function"
+ }
+
+ var function map[string]json.RawMessage
+ if json.Unmarshal(functionRaw, &function) != nil || function == nil {
+ return false, "invalid_tool_function"
+ }
+ for field := range function {
+ switch field {
+ case "name", "description", "parameters", "strict":
+ default:
+ return false, "unsafe_tool_function_field_" + field
+ }
+ }
+ var name string
+ if rawName, exists := function["name"]; !exists || json.Unmarshal(rawName, &name) != nil || strings.TrimSpace(name) == "" {
+ return false, "invalid_tool_function_name"
+ }
+ if rawDescription, exists := function["description"]; exists {
+ var description string
+ if json.Unmarshal(rawDescription, &description) != nil {
+ return false, "invalid_tool_function_description"
+ }
+ }
+ var parameters map[string]json.RawMessage
+ if rawParameters, exists := function["parameters"]; !exists || json.Unmarshal(rawParameters, ¶meters) != nil || parameters == nil {
+ return false, "invalid_tool_function_parameters"
+ }
+ if rawStrict, exists := function["strict"]; exists {
+ var strict bool
+ if json.Unmarshal(rawStrict, &strict) != nil {
+ return false, "invalid_tool_function_strict"
+ }
+ }
+ }
+ return true, ""
+}
+
+func grokChatToolChoiceBridgeable(raw json.RawMessage) (bool, string) {
+ if strings.TrimSpace(string(raw)) == "null" {
+ return true, ""
+ }
+ var choice string
+ if json.Unmarshal(raw, &choice) != nil {
+ return false, "unsupported_tool_choice"
+ }
+ switch choice {
+ case "auto", "none", "required":
+ return true, ""
+ default:
+ return false, "unsupported_tool_choice"
+ }
+}
+
+func grokChatHasFunctionDeclarations(root map[string]json.RawMessage) bool {
+ for _, field := range []string{"tools", "functions"} {
+ raw, exists := root[field]
+ if !exists {
+ continue
+ }
+ var declarations []json.RawMessage
+ if json.Unmarshal(raw, &declarations) == nil && len(declarations) > 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func grokChatMessageFieldsBridgeable(message map[string]json.RawMessage, allowedFields ...string) (bool, string) {
+ allowed := make(map[string]struct{}, len(allowedFields))
+ for _, field := range allowedFields {
+ allowed[field] = struct{}{}
+ }
+ for field := range message {
+ if _, ok := allowed[field]; !ok {
+ return false, "unsafe_message_field_" + field
+ }
+ }
+ return true, ""
+}
+
+func grokChatRequiredMessageContentBridgeable(raw json.RawMessage) (bool, string) {
+ var content string
+ if json.Unmarshal(raw, &content) == nil {
+ if strings.TrimSpace(content) == "" {
+ return false, "empty_message_content"
+ }
+ return true, ""
+ }
+ // Structured content: only allow arrays whose parts are text or
+ // image_url. These are losslessly convertible to Responses input_text/
+ // input_image parts, so the bridge preserves Chat Completions semantics.
+ return grokChatStructuredContentBridgeable(raw)
+}
+
+func grokChatAssistantToolCallsBridgeable(raw json.RawMessage) (int, string) {
+ if strings.TrimSpace(string(raw)) == "null" {
+ return 0, ""
+ }
+ var calls []map[string]json.RawMessage
+ if json.Unmarshal(raw, &calls) != nil {
+ return 0, "invalid_tool_calls"
+ }
+ for _, call := range calls {
+ if call == nil {
+ return 0, "invalid_tool_call"
+ }
+ for field := range call {
+ switch field {
+ case "id", "type", "function", "index":
+ default:
+ return 0, "unsafe_tool_call_field_" + field
+ }
+ }
+ if rawIndex, exists := call["index"]; exists {
+ var index *int
+ if json.Unmarshal(rawIndex, &index) != nil || (index != nil && *index < 0) {
+ return 0, "invalid_tool_call_index"
+ }
+ }
+ var callID string
+ if rawID, exists := call["id"]; !exists || json.Unmarshal(rawID, &callID) != nil || strings.TrimSpace(callID) == "" {
+ return 0, "invalid_tool_call_id"
+ }
+ var callType string
+ if rawType, exists := call["type"]; !exists || json.Unmarshal(rawType, &callType) != nil || callType != "function" {
+ return 0, "unsupported_tool_call_type"
+ }
+ var function map[string]json.RawMessage
+ if rawFunction, exists := call["function"]; !exists || json.Unmarshal(rawFunction, &function) != nil || function == nil {
+ return 0, "invalid_tool_call_function"
+ }
+ for field := range function {
+ if field != "name" && field != "arguments" {
+ return 0, "unsafe_tool_call_function_field_" + field
+ }
+ }
+ var name string
+ if rawName, exists := function["name"]; !exists || json.Unmarshal(rawName, &name) != nil || strings.TrimSpace(name) == "" {
+ return 0, "invalid_tool_call_function_name"
+ }
+ var arguments string
+ if rawArguments, exists := function["arguments"]; !exists || json.Unmarshal(rawArguments, &arguments) != nil || !json.Valid([]byte(arguments)) {
+ return 0, "invalid_tool_call_arguments"
+ }
+ }
+ return len(calls), ""
+}
+
func grokChatStructuredContentBridgeable(raw json.RawMessage) (bool, string) {
var parts []map[string]json.RawMessage
if err := json.Unmarshal(raw, &parts); err != nil {
@@ -199,14 +456,6 @@ func grokChatStructuredContentBridgeable(raw json.RawMessage) (bool, string) {
return true, ""
}
-func grokChatNullOrEmptyArray(raw json.RawMessage) bool {
- if strings.TrimSpace(string(raw)) == "null" {
- return true
- }
- var values []json.RawMessage
- return json.Unmarshal(raw, &values) == nil && len(values) == 0
-}
-
func grokChatNullOrNone(raw json.RawMessage) bool {
if strings.TrimSpace(string(raw)) == "null" {
return true
@@ -215,14 +464,34 @@ func grokChatNullOrNone(raw json.RawMessage) bool {
return json.Unmarshal(raw, &value) == nil && strings.EqualFold(strings.TrimSpace(value), "none")
}
-func grokChatCacheIntentBody(body []byte) ([]byte, error) {
+func grokChatJSONNull(raw json.RawMessage) bool {
+ return strings.TrimSpace(string(raw)) == "null"
+}
+
+func grokChatNullOrEmptyArray(raw json.RawMessage) bool {
+ if strings.TrimSpace(string(raw)) == "null" {
+ return true
+ }
+ var values []json.RawMessage
+ return json.Unmarshal(raw, &values) == nil && len(values) == 0
+}
+
+func grokChatResponsesCacheIntentBody(body []byte) ([]byte, error) {
+ // An empty Chat tools array is omitted by the Responses converter. In that
+ // case auto/none is also a semantic no-op and must not suppress the normal
+ // tool-free cache route. Non-empty converted tools are always kept intact.
+ if gjson.GetBytes(body, "tools").Exists() {
+ return append([]byte(nil), body...), nil
+ }
+ choice := gjson.GetBytes(body, "tool_choice")
+ if !choice.Exists() || choice.Type != gjson.String || (choice.String() != "auto" && choice.String() != "none") {
+ return append([]byte(nil), body...), nil
+ }
var root map[string]json.RawMessage
if err := json.Unmarshal(body, &root); err != nil {
return nil, err
}
- for _, field := range []string{"tools", "tool_choice", "functions", "function_call"} {
- delete(root, field)
- }
+ delete(root, "tool_choice")
return json.Marshal(root)
}
@@ -268,6 +537,10 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
}
responsesReq.Model = upstreamModel
responsesReq.Stream = true
+ // Keep Chat and native Responses paths aligned for OpenAI-compatible
+ // service_tier aliases (for example, "fast" -> "priority"). Unknown
+ // values are omitted by the shared normalizer instead of reaching xAI.
+ normalizeResponsesRequestServiceTier(responsesReq)
// These fields are useful to Codex but are not needed by the Grok CLI
// protocol. Keep the bridge request as close as possible to native Grok.
responsesReq.Include = nil
@@ -277,18 +550,25 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
if err != nil {
return nil, fmt.Errorf("marshal grok responses bridge request: %w", err)
}
+ // Preserve the converted Responses intent before Grok capability
+ // sanitization. Cache routing must see the actual client function tools,
+ // not the nested Chat Completions declarations and not a tool-free copy.
+ intentBody, err := grokChatResponsesCacheIntentBody(responsesBody)
+ if err != nil {
+ return nil, fmt.Errorf("normalize grok responses bridge cache intent: %w", err)
+ }
responsesBody, err = patchGrokResponsesBody(responsesBody, upstreamModel)
if err != nil {
return nil, fmt.Errorf("patch grok responses bridge request: %w", err)
}
- intentBody, err := grokChatCacheIntentBody(body)
- if err != nil {
- return nil, fmt.Errorf("normalize grok responses bridge tool intent: %w", err)
- }
responsesBody, err = applyGrokResponsesCacheIdentity(responsesBody, intentBody, cacheIdentity, true)
if err != nil {
return nil, fmt.Errorf("apply grok responses bridge cache identity: %w", err)
}
+ responsesBody, err = applyGrokFreeRequestToolCacheRoute(c, responsesBody, intentBody, account, cacheIdentity)
+ if err != nil {
+ return nil, fmt.Errorf("apply grok responses bridge function-tool cache route: %w", err)
+ }
updatedBody, policyErr := s.applyOpenAIFastPolicyToBody(ctx, account, upstreamModel, responsesBody)
if policyErr != nil {
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 86fc45e952..188e38f702 100644
--- a/backend/internal/service/openai_gateway_grok_chat_bridge_test.go
+++ b/backend/internal/service/openai_gateway_grok_chat_bridge_test.go
@@ -39,6 +39,31 @@ func TestGrokChatResponsesBridgeEligibility(t *testing.T) {
body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"stream":true,"stream_options":{"include_usage":true},"max_completion_tokens":256,"temperature":0.2,"top_p":0.9,"prompt_cache_key":"session","tools":[],"functions":null,"tool_choice":"none"}`,
want: true,
},
+ {
+ name: "Trae compatible fields",
+ body: `{"model":"grok","messages":[{"role":"user","content":"return json"}],"instructions":"Be concise","response_format":{"type":"json_object"},"service_tier":"fast","stop":null,"reasoning_effort":null}`,
+ want: true,
+ },
+ {
+ name: "Trae nullable SDK fields",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"instructions":null,"response_format":null,"service_tier":null,"stop":null,"reasoning_effort":null}`,
+ want: true,
+ },
+ {
+ name: "invalid instructions fall back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"instructions":{"text":"invalid"}}`,
+ reason: "invalid_instructions",
+ },
+ {
+ name: "invalid response format falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"response_format":"json_object"}`,
+ reason: "invalid_response_format",
+ },
+ {
+ name: "invalid service tier falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"service_tier":1}`,
+ reason: "invalid_service_tier",
+ },
{
name: "stop falls back",
body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"stop":"done"}`,
@@ -75,15 +100,80 @@ func TestGrokChatResponsesBridgeEligibility(t *testing.T) {
reason: "empty_message_content",
},
{
- name: "function tools fall back",
- body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[{"type":"function","function":{"name":"lookup"}}]}`,
- reason: "unsupported_tools",
+ name: "function tools bridge",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"},"strict":false}}]}`,
+ want: true,
},
{
- name: "automatic tool choice falls back",
- body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[],"tool_choice":"auto"}`,
+ name: "legacy functions fall back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"functions":[{"name":"lookup","parameters":{"type":"object"}}]}`,
+ reason: "unsupported_functions",
+ },
+ {
+ name: "automatic tool choice bridges",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[],"tool_choice":"auto"}`,
+ want: true,
+ },
+ {
+ name: "required tool choice without tools falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[],"tool_choice":"required"}`,
+ reason: "required_tool_choice_without_tools",
+ },
+ {
+ name: "tool history bridges",
+ body: `{"model":"grok","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_lookup","type":"function","function":{"name":"lookup","arguments":"{\"key\":\"alpha\"}"}}]},{"role":"tool","tool_call_id":"call_lookup","content":"{\"value\":\"ok\"}"},{"role":"user","content":"summarize"}],"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}],"tool_choice":"auto","parallel_tool_calls":true}`,
+ want: true,
+ },
+ {
+ name: "Trae reasoning and indexed tool history bridges",
+ body: `{"model":"grok","messages":[{"role":"assistant","content":null,"reasoning_content":"I should call lookup","tool_calls":[{"index":0,"id":"call_lookup","type":"function","function":{"name":"lookup","arguments":"{\"key\":\"alpha\"}"}}]},{"role":"tool","tool_call_id":"call_lookup","content":"{\"value\":\"ok\"}"},{"role":"user","content":"summarize"}],"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}]}`,
+ want: true,
+ },
+ {
+ name: "reasoning only assistant history bridges",
+ body: `{"model":"grok","messages":[{"role":"assistant","reasoning_content":"Prior reasoning"},{"role":"user","content":"continue"}]}`,
+ want: true,
+ },
+ {
+ name: "invalid reasoning content falls back",
+ body: `{"model":"grok","messages":[{"role":"assistant","reasoning_content":{"text":"invalid"}},{"role":"user","content":"continue"}]}`,
+ reason: "invalid_reasoning_content",
+ },
+ {
+ name: "negative tool call index falls back",
+ body: `{"model":"grok","messages":[{"role":"assistant","content":null,"tool_calls":[{"index":-1,"id":"call_lookup","type":"function","function":{"name":"lookup","arguments":"{}"}}]}]}`,
+ reason: "invalid_tool_call_index",
+ },
+ {
+ name: "unknown tool type falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[{"type":"web_search","function":{"name":"lookup","parameters":{"type":"object"}}}]}`,
+ reason: "unsupported_tool_type",
+ },
+ {
+ name: "missing tool schema falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[{"type":"function","function":{"name":"lookup"}}]}`,
+ reason: "invalid_tool_function_parameters",
+ },
+ {
+ name: "named tool choice falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}],"tool_choice":{"type":"function","function":{"name":"lookup"}}}`,
reason: "unsupported_tool_choice",
},
+ {
+ name: "invalid tool call arguments fall back",
+ body: `{"model":"grok","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_lookup","type":"function","function":{"name":"lookup","arguments":"{"}}]}]}`,
+ reason: "invalid_tool_call_arguments",
+ },
+ {
+ name: "tool result without call id falls back",
+ body: `{"model":"grok","messages":[{"role":"tool","content":"ok"}]}`,
+ reason: "invalid_tool_call_id",
+ },
+ {
+ name: "non boolean parallel tool calls falls back",
+ body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"parallel_tool_calls":"true"}`,
+ reason: "invalid_parallel_tool_calls",
+ },
{
name: "reasoning effort falls back because conversion adds summary",
body: `{"model":"grok","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high"}`,
@@ -100,9 +190,9 @@ func TestGrokChatResponsesBridgeEligibility(t *testing.T) {
reason: "empty_message_content",
},
{
- name: "tool history falls back",
+ name: "empty tool history falls back",
body: `{"model":"grok","messages":[{"role":"assistant","content":"","tool_calls":[]}]}`,
- reason: "unsafe_message_field_tool_calls",
+ reason: "empty_message_content",
},
{
name: "unknown field falls back",
@@ -183,6 +273,116 @@ func TestForwardGrokChatViaResponsesNonStreamingCachesAndReturnsChat(t *testing.
require.NotNil(t, repo.updates[account.ID][grokQuotaSnapshotExtraKey])
}
+func TestForwardGrokChatViaResponsesTraeToolHistoryKeepsCacheRoute(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ firstTurnBody := []byte(`{"model":"grok","messages":[{"role":"system","content":"Be concise"},{"role":"user","content":"Find alpha"}],"stream":false,"prompt_cache_key":"trae-session","tools":[{"type":"function","function":{"name":"lookup","description":"Lookup a value","parameters":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]},"strict":false}}],"tool_choice":"auto","parallel_tool_calls":true}`)
+ body := []byte(`{"model":"grok","messages":[{"role":"system","content":"Be concise"},{"role":"user","content":"Find alpha"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_lookup","type":"function","function":{"name":"lookup","arguments":"{\"key\":\"alpha\"}"}}]},{"role":"tool","tool_call_id":"call_lookup","content":"{\"value\":\"ok\"}"},{"role":"user","content":"Summarize"}],"stream":false,"prompt_cache_key":"trae-session","tools":[{"type":"function","function":{"name":"lookup","description":"Lookup a value","parameters":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]},"strict":false}}],"tool_choice":"auto","parallel_tool_calls":true}`)
+ recorder := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(recorder)
+ c.Request = httptest.NewRequest(http.MethodPost, grokChatRawEndpoint, bytes.NewReader(body))
+ c.Request.Header.Set(grokClientToolCacheOptInHeader, "prefer-cache")
+ c.Set("api_key", &APIKey{ID: 7151})
+
+ account := grokChatBridgeTestAccount(715)
+ account.Credentials["subscription_tier"] = "free"
+ repo := &grokQuotaAccountRepo{mockAccountRepoForPlatform: &mockAccountRepoForPlatform{
+ accountsByID: map[int64]*Account{account.ID: account},
+ }}
+ upstream := &httpUpstreamRecorder{resp: grokChatBridgeCompletedResponse("resp_grok_chat_trae", 8192)}
+ svc := &OpenAIGatewayService{
+ httpUpstream: upstream,
+ grokTokenProvider: NewGrokTokenProvider(repo, nil),
+ accountRepo: repo,
+ }
+
+ firstTurnIdentity := resolveGrokCacheIdentity(c, firstTurnBody, "", "grok-4.5")
+ extendedTurnIdentity := resolveGrokCacheIdentity(c, body, "", "grok-4.5")
+ require.NotEmpty(t, firstTurnIdentity)
+ require.Equal(t, firstTurnIdentity, extendedTurnIdentity)
+
+ result, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.Equal(t, xai.DefaultCLIBaseURL+"/responses", upstream.lastReq.URL.String())
+ require.Equal(t, grokChatResponsesEndpoint, result.UpstreamEndpoint)
+ require.Equal(t, extendedTurnIdentity, gjson.GetBytes(upstream.lastBody, "prompt_cache_key").String())
+ require.Equal(t, extendedTurnIdentity, upstream.lastReq.Header.Get(grokConversationIDHeader))
+
+ tools := gjson.GetBytes(upstream.lastBody, "tools").Array()
+ require.Len(t, tools, 3)
+ require.Equal(t, "function", tools[0].Get("type").String())
+ require.Equal(t, "lookup", tools[0].Get("name").String())
+ require.Equal(t, "Lookup a value", tools[0].Get("description").String())
+ require.Equal(t, "string", tools[0].Get("parameters.properties.key.type").String())
+ require.True(t, tools[0].Get("strict").Exists())
+ require.False(t, tools[0].Get("strict").Bool())
+ require.Equal(t, "web_search", tools[1].Get("type").String())
+ require.Equal(t, "x_search", tools[2].Get("type").String())
+ require.Equal(t, "auto", gjson.GetBytes(upstream.lastBody, "tool_choice").String())
+ require.True(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Bool())
+
+ require.Equal(t, "function_call", gjson.GetBytes(upstream.lastBody, "input.2.type").String())
+ require.Equal(t, "call_lookup", gjson.GetBytes(upstream.lastBody, "input.2.call_id").String())
+ require.Equal(t, "lookup", gjson.GetBytes(upstream.lastBody, "input.2.name").String())
+ require.Equal(t, `{"key":"alpha"}`, gjson.GetBytes(upstream.lastBody, "input.2.arguments").String())
+ require.Equal(t, "function_call_output", gjson.GetBytes(upstream.lastBody, "input.3.type").String())
+ require.Equal(t, "call_lookup", gjson.GetBytes(upstream.lastBody, "input.3.call_id").String())
+ require.Equal(t, `{"value":"ok"}`, gjson.GetBytes(upstream.lastBody, "input.3.output").String())
+}
+
+func TestForwardGrokChatViaResponsesTraeCompatibilityFieldsKeepCacheRoute(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ firstTurnBody := []byte(`{"model":"grok","messages":[{"role":"user","content":"Find alpha"}],"instructions":"Return concise JSON","stream":false,"response_format":{"type":"json_object"},"service_tier":"fast","stop":null,"reasoning_effort":null,"tools":[{"type":"function","function":{"name":"lookup","description":"Lookup a value","parameters":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]}}}],"tool_choice":"auto","parallel_tool_calls":true}`)
+ body := []byte(`{"model":"grok","messages":[{"role":"user","content":"Find alpha"},{"role":"assistant","content":null,"reasoning_content":"I should use lookup","tool_calls":[{"index":0,"id":"call_lookup","type":"function","function":{"name":"lookup","arguments":"{\"key\":\"alpha\"}"}}]},{"role":"tool","tool_call_id":"call_lookup","content":"{\"value\":\"ok\"}"},{"role":"user","content":"Summarize"}],"instructions":"Return concise JSON","stream":false,"response_format":{"type":"json_object"},"service_tier":"fast","stop":null,"reasoning_effort":null,"tools":[{"type":"function","function":{"name":"lookup","description":"Lookup a value","parameters":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]}}}],"tool_choice":"auto","parallel_tool_calls":true}`)
+ recorder := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(recorder)
+ c.Request = httptest.NewRequest(http.MethodPost, grokChatRawEndpoint, bytes.NewReader(body))
+ c.Set("api_key", &APIKey{ID: 7161})
+
+ account := grokChatBridgeTestAccount(716)
+ account.Credentials["subscription_tier"] = "free"
+ repo := &grokQuotaAccountRepo{mockAccountRepoForPlatform: &mockAccountRepoForPlatform{
+ accountsByID: map[int64]*Account{account.ID: account},
+ }}
+ upstream := &httpUpstreamRecorder{resp: grokChatBridgeCompletedResponse("resp_grok_chat_trae_compat", 12288)}
+ svc := &OpenAIGatewayService{
+ httpUpstream: upstream,
+ grokTokenProvider: NewGrokTokenProvider(repo, nil),
+ accountRepo: repo,
+ }
+
+ firstTurnIdentity := resolveGrokCacheIdentity(c, firstTurnBody, "", "grok-4.5")
+ extendedTurnIdentity := resolveGrokCacheIdentity(c, body, "", "grok-4.5")
+ require.NotEmpty(t, firstTurnIdentity)
+ require.Equal(t, firstTurnIdentity, extendedTurnIdentity)
+
+ result, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.Equal(t, xai.DefaultCLIBaseURL+"/responses", upstream.lastReq.URL.String())
+ require.Equal(t, grokChatResponsesEndpoint, result.UpstreamEndpoint)
+ require.Equal(t, 12288, result.Usage.CacheReadInputTokens)
+ require.Equal(t, extendedTurnIdentity, gjson.GetBytes(upstream.lastBody, "prompt_cache_key").String())
+ require.Equal(t, extendedTurnIdentity, upstream.lastReq.Header.Get(grokConversationIDHeader))
+ require.Equal(t, "Return concise JSON", gjson.GetBytes(upstream.lastBody, "instructions").String())
+ require.Equal(t, "json_object", gjson.GetBytes(upstream.lastBody, "text.format.type").String())
+ require.Equal(t, "priority", gjson.GetBytes(upstream.lastBody, "service_tier").String())
+ require.False(t, gjson.GetBytes(upstream.lastBody, "stop").Exists())
+ require.False(t, gjson.GetBytes(upstream.lastBody, "reasoning").Exists())
+ require.Contains(t, gjson.GetBytes(upstream.lastBody, "input.1.content.0.text").String(), "
+ {{ t('admin.accounts.grokClientToolCache.hint') }} +
+