fix(grok): enable free prompt cache for Responses function tools

Reuse applyGrokFreeMessagesFunctionToolCacheRoute on native /v1/responses
and the Grok WS HTTP bridge so Free OAuth requests with client function
tools get the same mixed-tools cache route as the Messages bridge
(append/convert web_search and x_search).

Also dedupe: Grok Build already declares function tools named web_search,
so naive append caused "Duplicate tool names: web_search". Convert those
function entries to native tool types and skip duplicates.

Only Free OAuth accounts (isKnownGrokFreeAccount); paid/unknown unchanged.
This commit is contained in:
rurucxk
2026-07-17 00:23:19 +08:00
parent bc2244c83f
commit 29e39b96e5
3 changed files with 35 additions and 6 deletions
@@ -61,6 +61,12 @@ func (s *OpenAIGatewayService) forwardGrokResponses(
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)
if err != nil {
return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", err)
}
token, _, err := s.getRequestCredential(ctx, c, account)
if err != nil {
@@ -102,8 +102,8 @@ func isGrokRequestContext(c *gin.Context) bool {
// Free OAuth requests without native search tools are routed by xAI to the
// non-cacheable build-free model. For otherwise tool-free requests, add the
// native tools with tool_choice=none: this selects the cache-capable tier
// without allowing an actual search. Explicit client tools are handled by the
// narrower Messages-only mixed-tools policy below.
// without allowing an actual search. Explicit client function tools are handled by
// applyGrokFreeMessagesFunctionToolCacheRoute (Messages bridge and native Responses).
func applyGrokResponsesCacheIdentity(body, intentSourceBody []byte, identity string, injectFreeTierTools bool) ([]byte, error) {
identity = strings.TrimSpace(identity)
if identity == "" {
@@ -263,17 +263,35 @@ func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) {
toolType := strings.TrimSpace(tool.Get("type").String())
switch toolType {
case "function":
if !tool.IsObject() || strings.TrimSpace(tool.Get("name").String()) == "" || tool.Get("function").Exists() {
name := strings.TrimSpace(tool.Get("name").String())
if !tool.IsObject() || name == "" || tool.Get("function").Exists() {
return body, nil
}
// 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 present[name] {
continue
}
raw, err := json.Marshal(map[string]string{"type": name})
if err != nil {
return nil, err
}
merged = append(merged, raw)
present[name] = true
continue
}
hasFunction = true
merged = append(merged, json.RawMessage(tool.Raw))
case "web_search", "x_search":
// Native tools may already be present when this helper is retried.
if present[toolType] {
continue
}
merged = append(merged, json.RawMessage(tool.Raw))
present[toolType] = true
default:
return body, nil
}
merged = append(merged, json.RawMessage(tool.Raw))
present[toolType] = true
}
if !hasFunction {
return body, nil
@@ -201,6 +201,11 @@ func (s *OpenAIGatewayService) proxyOpenAIWSHTTPBridgeTurn(
releaseUpstreamCtx()
return nil, fmt.Errorf("apply grok prompt cache identity: %w", err)
}
body, err = applyGrokFreeMessagesFunctionToolCacheRoute(body, grokIntentSourceBody, account, grokCacheIdentity)
if err != nil {
releaseUpstreamCtx()
return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", err)
}
upstreamReq, err = buildGrokResponsesRequest(upstreamCtx, c, account, body, token, grokCacheIdentity, s.cfg)
} else {
upstreamReq, err = s.buildUpstreamRequestOpenAIPassthrough(upstreamCtx, c, account, body, token)