package executor import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strconv" "strings" "time" "github.com/google/uuid" xaiauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/xai" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/signature" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/tiktoken-go/tokenizer" ) var ( xaiDataTag = []byte("data:") xaiEventTag = []byte("event:") ) const ( xaiImageHandlerType = "openai-image" xaiVideoHandlerType = "openai-video" xaiCustomToolType = "custom" xaiFunctionToolType = "function" xaiImageGenerationToolType = "image_generation" xaiNamespaceToolType = "namespace" xaiToolSearchType = "tool_search" xaiWebSearchToolType = "web_search" xaiXSearchToolType = "x_search" // Codex Desktop injects codex_app.automation_update with a large oneOf+$ref // schema. xAI's free/build Responses path accepts the HTTP request but never // emits SSE when that schema is present, so Desktop hangs on "thinking". xaiCodexAppNamespaceName = "codex_app" xaiAutomationUpdateToolName = "automation_update" // Permissive placeholder schema: keeps the tool callable without the hang. xaiSafeFunctionParameters = `{"type":"object","properties":{},"additionalProperties":true}` xaiImagesGenerationsPath = "/images/generations" xaiImagesEditsPath = "/images/edits" xaiDefaultImageEndpointPath = xaiImagesGenerationsPath xaiVideosGenerationsPath = "/videos/generations" xaiVideosEditsPath = "/videos/edits" xaiVideosExtensionsPath = "/videos/extensions" xaiVideosPath = "/videos" xaiIdempotencyKeyMetaKey = "idempotency_key" xaiComposerModelPrefix = "grok-composer-" xaiTokenAuthHeader = "X-XAI-Token-Auth" xaiTokenAuthValue = "xai-grok-cli" xaiClientVersionHeader = "x-grok-client-version" // Keep in sync with the current Grok CLI client version that chat-proxy expects. xaiClientVersionValue = "0.2.93" // xaiUsingAPIAttr enables the official API path for non-media HTTP chat. xaiUsingAPIAttr = "using_api" ) // Always inject native x_search when the client did not declare it so Grok can // run X Search server-side. Internal subtool traces are still filtered downstream // when this native tool is present (see filterInternalXSearch). var xaiXSearchToolJSON = []byte(`{"type":"x_search"}`) // XAIExecutor is a stateless executor for xAI Grok's Responses API. type XAIExecutor struct { cfg *config.Config } // NewXAIExecutor creates a new xAI executor. func NewXAIExecutor(cfg *config.Config) *XAIExecutor { return &XAIExecutor{cfg: cfg} } // Identifier returns the provider identifier. func (e *XAIExecutor) Identifier() string { return "xai" } // PrepareRequest injects xAI credentials into the outgoing HTTP request. func (e *XAIExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error { if req == nil { return nil } token, _ := xaiCreds(auth) if strings.TrimSpace(token) != "" { req.Header.Set("Authorization", "Bearer "+token) } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(req, attrs) return nil } // HttpRequest injects xAI credentials into the request and executes it. func (e *XAIExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) { if req == nil { return nil, fmt.Errorf("xai executor: request is nil") } if ctx == nil { ctx = req.Context() } httpReq := req.WithContext(ctx) if errPrepare := e.PrepareRequest(httpReq, auth); errPrepare != nil { return nil, errPrepare } httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } func (e *XAIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { if opts.Alt == "responses/compact" { return e.executeCompact(ctx, auth, req, opts) } if endpointPath := xaiImageEndpointPath(opts); endpointPath != "" { return e.executeImages(ctx, auth, req, endpointPath) } if xaiIsVideoRequest(opts) { return e.executeVideos(ctx, auth, req, opts) } token, _ := xaiCreds(auth) baseURL := xaiChatBaseURL(auth) logXAIResolvedBaseURL(ctx, baseURL) prepared, err := e.prepareResponsesRequest(ctx, req, opts, true) if err != nil { return resp, err } reporter := helps.NewExecutorUsageReporter(ctx, e, prepared.baseModel, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(prepared.body, e.Identifier()) url := strings.TrimSuffix(baseURL, "/") + "/responses" httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(prepared.body)) if err != nil { return resp, err } applyXAIChatHeaders(httpReq, auth, token, true, prepared.sessionID) e.recordXAIRequest(ctx, auth, url, httpReq.Header.Clone(), prepared.body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, err := httpClient.Do(httpReq) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("xai executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) return resp, xaiStatusErr(httpResp.StatusCode, data) } data, err := io.ReadAll(httpResp.Body) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } helps.AppendAPIResponseChunk(ctx, e.cfg, data) outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte responseFilter := newXAIInternalXSearchResponseFilter(prepared.filterInternalXSearch, prepared.clientDeclaredTools) for _, line := range bytes.Split(data, []byte("\n")) { if !bytes.HasPrefix(line, xaiDataTag) { continue } eventData := xaiNormalizeReasoningSummaryData(bytes.TrimSpace(line[len(xaiDataTag):])) eventData = restoreXAINamespaceToolCalls(eventData, prepared.namespaceTools) eventData = responseFilter.apply(eventData) if len(eventData) == 0 { continue } switch gjson.GetBytes(eventData, "type").String() { case "response.output_item.done": xaiCollectOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.completed": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } completedData := xaiPatchCompletedOutput(eventData, outputItemsByIndex, outputItemsFallback) completedData = xaiNormalizeReasoningSummaryData(completedData) cacheXAIReasoningReplayFromCompleted(ctx, prepared.replayScope, completedData) var param any out := sdktranslator.TranslateNonStream(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, completedData, ¶m) return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil } } return resp, statusErr{code: http.StatusRequestTimeout, msg: "xai stream error: stream disconnected before response.completed"} } func (e *XAIExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { prepared, data, headers, errCompact := e.executeCompactRequest(ctx, auth, req, opts) if errCompact != nil { return resp, errCompact } var param any out := sdktranslator.TranslateNonStream(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, data, ¶m) return cliproxyexecutor.Response{Payload: out, Headers: headers}, nil } func (e *XAIExecutor) executeCompactRequest(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (*xaiPreparedRequest, []byte, http.Header, error) { token, _ := xaiCreds(auth) baseURL := xaiChatBaseURL(auth) logXAIResolvedBaseURL(ctx, baseURL) prepared, err := e.prepareResponsesRequestTo(ctx, req, opts, false, sdktranslator.FormatOpenAIResponse) if err != nil { return nil, nil, nil, err } prepared.body, _ = sjson.DeleteBytes(prepared.body, "stream") prepared.body, _ = sjson.DeleteBytes(prepared.body, "tools") prepared.body = xaiRemoveInputItemsByType(prepared.body, "compaction_trigger") reporter := helps.NewExecutorUsageReporter(ctx, e, prepared.baseModel, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(prepared.body, e.Identifier()) requestURL := strings.TrimSuffix(baseURL, "/") + "/responses/compact" httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewReader(prepared.body)) if err != nil { return nil, nil, nil, err } applyXAIChatHeaders(httpReq, auth, token, false, prepared.sessionID) e.recordXAIRequest(ctx, auth, requestURL, httpReq.Header.Clone(), prepared.body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, err := httpClient.Do(httpReq) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return nil, nil, nil, err } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("xai executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, err := io.ReadAll(httpResp.Body) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return nil, nil, nil, err } helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = xaiStatusErr(httpResp.StatusCode, data) return nil, nil, nil, err } reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) reporter.EnsurePublished(ctx) clearXAIReasoningReplayAfterCompaction(ctx, prepared.replayScope) return prepared, data, httpResp.Header.Clone(), nil } func (e *XAIExecutor) executeCompactionTriggerStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) { prepared, data, headers, err := e.executeCompactRequest(ctx, auth, req, opts) if err != nil { return nil, err } headers = headers.Clone() if headers == nil { headers = make(http.Header) } headers.Set("Content-Type", "text/event-stream") chunks := xaiBuildCompactionTriggerStreamChunks(prepared, data) out := make(chan cliproxyexecutor.StreamChunk, len(chunks)) for _, chunk := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: chunk} } close(out) return &cliproxyexecutor.StreamResult{Headers: headers, Chunks: out}, nil } func xaiInputHasItemType(body []byte, itemType string) bool { input := gjson.GetBytes(body, "input") if !input.IsArray() { return false } for _, item := range input.Array() { if item.Get("type").String() == itemType { return true } } return false } func xaiRemoveInputItemsByType(body []byte, itemType string) []byte { input := gjson.GetBytes(body, "input") if !input.IsArray() { return body } var buf bytes.Buffer buf.WriteByte('[') kept := 0 for _, item := range input.Array() { if item.Get("type").String() == itemType { continue } if kept > 0 { buf.WriteByte(',') } buf.WriteString(item.Raw) kept++ } buf.WriteByte(']') updated, err := sjson.SetRawBytes(body, "input", buf.Bytes()) if err != nil { return body } return updated } func xaiBuildCompactionTriggerStreamChunks(prepared *xaiPreparedRequest, compactData []byte) [][]byte { responseID := xaiCompactionResponseID(compactData) now := time.Now().Unix() createdAt := gjson.GetBytes(compactData, "created_at").Int() if createdAt == 0 { createdAt = now } completedAt := gjson.GetBytes(compactData, "completed_at").Int() if completedAt == 0 { completedAt = now } item := xaiCompactionOutputItem(compactData, responseID) output := make([]byte, 0, len(item)+2) output = append(output, '[') output = append(output, item...) output = append(output, ']') createdResponse := xaiBuildCompactionBaseResponse(prepared, compactData, responseID, createdAt, "in_progress") inProgressResponse := xaiBuildCompactionBaseResponse(prepared, compactData, responseID, createdAt, "in_progress") completedResponse := xaiBuildCompactionBaseResponse(prepared, compactData, responseID, createdAt, "completed") completedResponse, _ = sjson.SetBytes(completedResponse, "completed_at", completedAt) completedResponse, _ = sjson.SetRawBytes(completedResponse, "output", output) if usage := gjson.GetBytes(compactData, "usage"); usage.Exists() { completedResponse, _ = sjson.SetRawBytes(completedResponse, "usage", []byte(usage.Raw)) } createdPayload := []byte(`{"type":"response.created","sequence_number":0}`) createdPayload, _ = sjson.SetRawBytes(createdPayload, "response", createdResponse) inProgressPayload := []byte(`{"type":"response.in_progress","sequence_number":1}`) inProgressPayload, _ = sjson.SetRawBytes(inProgressPayload, "response", inProgressResponse) addedPayload := []byte(`{"type":"response.output_item.added","sequence_number":2,"output_index":0}`) addedPayload, _ = sjson.SetRawBytes(addedPayload, "item", item) keepalivePayload := []byte(`{"type":"keepalive","sequence_number":3}`) donePayload := []byte(`{"type":"response.output_item.done","sequence_number":4,"output_index":0}`) donePayload, _ = sjson.SetRawBytes(donePayload, "item", item) completedPayload := []byte(`{"type":"response.completed","sequence_number":5}`) completedPayload, _ = sjson.SetRawBytes(completedPayload, "response", completedResponse) return [][]byte{ xaiBuildSSEFrame("response.created", createdPayload), xaiBuildSSEFrame("response.in_progress", inProgressPayload), xaiBuildSSEFrame("response.output_item.added", addedPayload), xaiBuildSSEFrame("keepalive", keepalivePayload), xaiBuildSSEFrame("response.output_item.done", donePayload), xaiBuildSSEFrame("response.completed", completedPayload), } } func xaiBuildCompactionBaseResponse(prepared *xaiPreparedRequest, compactData []byte, responseID string, createdAt int64, status string) []byte { response := []byte(`{"id":"","object":"response","created_at":0,"status":"","background":false,"error":null,"incomplete_details":null,"output":[]}`) response, _ = sjson.SetBytes(response, "id", responseID) response, _ = sjson.SetBytes(response, "created_at", createdAt) response, _ = sjson.SetBytes(response, "status", status) if model := gjson.GetBytes(compactData, "model").String(); model != "" { response, _ = sjson.SetBytes(response, "model", model) } else if prepared != nil && prepared.baseModel != "" { response, _ = sjson.SetBytes(response, "model", prepared.baseModel) } if prepared == nil { return response } for _, field := range []string{ "instructions", "max_output_tokens", "max_tool_calls", "parallel_tool_calls", "previous_response_id", "prompt_cache_key", "reasoning", "text", "tool_choice", "tools", "top_logprobs", "top_p", "truncation", "user", "metadata", } { if value := gjson.GetBytes(prepared.body, field); value.Exists() { response, _ = sjson.SetRawBytes(response, field, []byte(value.Raw)) } } return response } func xaiCompactionOutputItem(compactData []byte, responseID string) []byte { itemResult := gjson.GetBytes(compactData, "output.0") item := []byte(`{"type":"compaction"}`) if itemResult.Exists() && itemResult.Type == gjson.JSON { item = []byte(itemResult.Raw) } if !gjson.GetBytes(item, "type").Exists() { item, _ = sjson.SetBytes(item, "type", "compaction") } if !gjson.GetBytes(item, "id").Exists() { item, _ = sjson.SetBytes(item, "id", xaiCompactionItemID(responseID)) } return item } func xaiCompactionResponseID(compactData []byte) string { if responseID := strings.TrimSpace(gjson.GetBytes(compactData, "id").String()); responseID != "" { if strings.HasPrefix(responseID, "resp_") { return responseID } return "resp_" + strings.TrimPrefix(responseID, "cmp_") } return fmt.Sprintf("resp_xai_compaction_%d", time.Now().UnixNano()) } func xaiCompactionItemID(responseID string) string { if suffix := strings.TrimPrefix(responseID, "resp_"); suffix != "" && suffix != responseID { return "cmp_" + suffix } return "cmp_" + responseID } func xaiBuildSSEFrame(eventName string, data []byte) []byte { out := make([]byte, 0, len(eventName)+len(data)+16) out = append(out, "event: "...) out = append(out, eventName...) out = append(out, '\n') out = append(out, "data: "...) out = append(out, data...) out = append(out, '\n', '\n') return out } func (e *XAIExecutor) executeImages(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, endpointPath string) (resp cliproxyexecutor.Response, err error) { model := strings.TrimSpace(gjson.GetBytes(req.Payload, "model").String()) if model == "" { model = strings.TrimSpace(req.Model) } reporter := helps.NewExecutorUsageReporter(ctx, e, model, auth) defer reporter.TrackFailure(ctx, &err) token, baseURL := xaiCreds(auth) if baseURL == "" { baseURL = xaiauth.DefaultAPIBaseURL } logXAIResolvedBaseURL(ctx, baseURL) if endpointPath == "" { endpointPath = xaiDefaultImageEndpointPath } url := strings.TrimSuffix(baseURL, "/") + endpointPath httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(req.Payload)) if err != nil { return resp, err } applyXAIHeaders(httpReq, auth, token, false, "") e.recordXAIRequest(ctx, auth, url, httpReq.Header.Clone(), req.Payload) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, err := httpClient.Do(httpReq) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("xai executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, err := io.ReadAll(httpResp.Body) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = xaiStatusErr(httpResp.StatusCode, data) return resp, err } reporter.EnsurePublished(ctx) return cliproxyexecutor.Response{Payload: data, Headers: httpResp.Header.Clone()}, nil } func (e *XAIExecutor) executeVideos(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { token, baseURL := xaiCreds(auth) if baseURL == "" { baseURL = xaiauth.DefaultAPIBaseURL } logXAIResolvedBaseURL(ctx, baseURL) method := http.MethodPost endpointPath := xaiVideosGenerationsPath var body io.Reader = bytes.NewReader(req.Payload) switch path := xaiVideoEndpointPath(opts); path { case xaiVideosGenerationsPath, xaiVideosEditsPath, xaiVideosExtensionsPath: endpointPath = path default: if requestID := strings.TrimSpace(gjson.GetBytes(req.Payload, "request_id").String()); requestID != "" { method = http.MethodGet endpointPath = xaiVideosPath + "/" + url.PathEscape(requestID) body = nil } } requestURL := strings.TrimSuffix(baseURL, "/") + endpointPath httpReq, err := http.NewRequestWithContext(ctx, method, requestURL, body) if err != nil { return resp, err } applyXAIHeaders(httpReq, auth, token, false, "") if method == http.MethodPost { key := xaiMetadataString(opts.Metadata, xaiIdempotencyKeyMetaKey) if key == "" && opts.Headers != nil { key = strings.TrimSpace(opts.Headers.Get("x-idempotency-key")) } if key != "" { httpReq.Header.Set("x-idempotency-key", key) } } e.recordXAIRequest(ctx, auth, requestURL, httpReq.Header.Clone(), req.Payload) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("xai executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, err := io.ReadAll(httpResp.Body) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) return resp, xaiStatusErr(httpResp.StatusCode, data) } return cliproxyexecutor.Response{Payload: data, Headers: httpResp.Header.Clone()}, nil } func (e *XAIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) { if opts.Alt == "responses/compact" { return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"} } if xaiInputHasItemType(req.Payload, "compaction_trigger") { return e.executeCompactionTriggerStream(ctx, auth, req, opts) } token, _ := xaiCreds(auth) baseURL := xaiChatBaseURL(auth) logXAIResolvedBaseURL(ctx, baseURL) prepared, err := e.prepareResponsesRequest(ctx, req, opts, true) if err != nil { return nil, err } reporter := helps.NewExecutorUsageReporter(ctx, e, prepared.baseModel, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(prepared.body, e.Identifier()) url := strings.TrimSuffix(baseURL, "/") + "/responses" httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(prepared.body)) if err != nil { return nil, err } applyXAIChatHeaders(httpReq, auth, token, true, prepared.sessionID) e.recordXAIRequest(ctx, auth, url, httpReq.Header.Clone(), prepared.body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, err := httpClient.Do(httpReq) if err != nil { helps.RecordAPIResponseError(ctx, e.cfg, err) return nil, err } helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { data, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("xai executor: close response body error: %v", errClose) } if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) return nil, xaiStatusErr(httpResp.StatusCode, data) } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("xai executor: close response body error: %v", errClose) } }() scanner := bufio.NewScanner(httpResp.Body) scanner.Buffer(nil, 52_428_800) var param any outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte responseFilter := newXAIInternalXSearchResponseFilter(prepared.filterInternalXSearch, prepared.clientDeclaredTools) var pendingEventLine []byte emitTranslatedLine := func(translatedLine []byte) bool { chunks := sdktranslator.TranslateStream(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, translatedLine, ¶m) for i := range chunks { select { case out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]}: case <-ctx.Done(): return false } } return true } for scanner.Scan() { line := scanner.Bytes() helps.AppendAPIResponseChunk(ctx, e.cfg, line) if bytes.HasPrefix(line, xaiEventTag) { if pendingEventLine != nil && !emitTranslatedLine(xaiNormalizeReasoningSummaryEventLine(pendingEventLine, "")) { return } pendingEventLine = bytes.Clone(line) continue } if bytes.HasPrefix(line, xaiDataTag) { eventDataList := xaiNormalizeReasoningSummaryDataEvents(bytes.TrimSpace(line[len(xaiDataTag):])) hasPendingEventLine := pendingEventLine != nil for i, eventData := range eventDataList { eventData = restoreXAINamespaceToolCalls(eventData, prepared.namespaceTools) eventData = responseFilter.apply(eventData) if len(eventData) == 0 { if hasPendingEventLine && i == 0 { pendingEventLine = nil } continue } normalizedEventName := gjson.GetBytes(eventData, "type").String() switch normalizedEventName { case "response.output_item.done": xaiCollectOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.completed": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } eventData = xaiPatchCompletedOutput(eventData, outputItemsByIndex, outputItemsFallback) eventData = xaiNormalizeReasoningSummaryData(eventData) cacheXAIReasoningReplayFromCompleted(ctx, prepared.replayScope, eventData) normalizedEventName = gjson.GetBytes(eventData, "type").String() } if hasPendingEventLine { eventLine := []byte("event: " + normalizedEventName) if i == 0 { eventLine = xaiNormalizeReasoningSummaryEventLine(pendingEventLine, normalizedEventName) pendingEventLine = nil } if !emitTranslatedLine(eventLine) { return } } if !emitTranslatedLine(append([]byte("data: "), eventData...)) { return } } continue } if pendingEventLine != nil { if !emitTranslatedLine(xaiNormalizeReasoningSummaryEventLine(pendingEventLine, "")) { return } pendingEventLine = nil } if !emitTranslatedLine(bytes.Clone(line)) { return } } if pendingEventLine != nil { emitTranslatedLine(xaiNormalizeReasoningSummaryEventLine(pendingEventLine, "")) } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) reporter.PublishFailure(ctx, errScan) select { case out <- cliproxyexecutor.StreamChunk{Err: errScan}: case <-ctx.Done(): } } }() return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil } // CountTokens estimates token count for xAI Responses requests. func (e *XAIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { prepared, err := e.prepareResponsesRequest(ctx, req, opts, false) if err != nil { return cliproxyexecutor.Response{}, err } enc, err := tokenizer.Get(tokenizer.Cl100kBase) if err != nil { return cliproxyexecutor.Response{}, fmt.Errorf("xai executor: tokenizer init failed: %w", err) } count, err := enc.Count(string(prepared.body)) if err != nil { return cliproxyexecutor.Response{}, fmt.Errorf("xai executor: token counting failed: %w", err) } usageJSON := fmt.Sprintf(`{"response":{"usage":{"input_tokens":%d,"output_tokens":0,"total_tokens":%d}}}`, count, count) translated := sdktranslator.TranslateTokenCount(ctx, prepared.to, prepared.responseFormat, int64(count), []byte(usageJSON)) return cliproxyexecutor.Response{Payload: translated}, nil } // Refresh refreshes xAI OAuth credentials using the stored refresh token. func (e *XAIExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("xai executor: refresh called") if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled { return refreshed, err } if auth == nil { return nil, statusErr{code: http.StatusInternalServerError, msg: "xai executor: auth is nil"} } refreshToken := xaiMetadataString(auth.Metadata, "refresh_token") if refreshToken == "" { return auth, nil } tokenEndpoint := xaiMetadataString(auth.Metadata, "token_endpoint") svc := xaiauth.NewXAIAuthWithProxyURL(e.cfg, auth.ProxyURL) td, err := svc.RefreshTokens(ctx, refreshToken, tokenEndpoint) if err != nil { return nil, err } if auth.Metadata == nil { auth.Metadata = make(map[string]any) } auth.Metadata["type"] = "xai" auth.Metadata["auth_kind"] = "oauth" auth.Metadata["access_token"] = td.AccessToken if td.RefreshToken != "" { auth.Metadata["refresh_token"] = td.RefreshToken } if td.IDToken != "" { auth.Metadata["id_token"] = td.IDToken } if td.TokenType != "" { auth.Metadata["token_type"] = td.TokenType } if td.ExpiresIn > 0 { auth.Metadata["expires_in"] = td.ExpiresIn } if td.Expire != "" { auth.Metadata["expired"] = td.Expire } if td.Email != "" { auth.Metadata["email"] = td.Email } if td.Subject != "" { auth.Metadata["sub"] = td.Subject } if tokenEndpoint != "" { auth.Metadata["token_endpoint"] = tokenEndpoint } if xaiMetadataString(auth.Metadata, "base_url") == "" { auth.Metadata["base_url"] = xaiauth.DefaultAPIBaseURL } auth.Metadata["last_refresh"] = time.Now().UTC().Format(time.RFC3339) if auth.Attributes == nil { auth.Attributes = make(map[string]string) } auth.Attributes["auth_kind"] = "oauth" if strings.TrimSpace(auth.Attributes["base_url"]) == "" { auth.Attributes["base_url"] = xaiauth.DefaultAPIBaseURL } return auth, nil } type xaiPreparedRequest struct { baseModel string from sdktranslator.Format responseFormat sdktranslator.Format to sdktranslator.Format originalPayload []byte body []byte namespaceTools map[string]xaiNamespaceToolRef clientDeclaredTools map[xaiClientToolKey]struct{} sessionID string replayScope xaiReasoningReplayScope filterInternalXSearch bool } type xaiNamespaceToolRef struct { namespace string name string } // xaiClientToolKey identifies a client-declared callable tool using the // post-restore Responses shape (short name + optional namespace) and the // effective upstream tool type after normalizeXAITool (client custom tools are // sent as function). Response call types are matched against this effective // kind so internal custom_tool_call traces are not exempted merely because a // client declared an ordinary function/custom tool with the same short name, // while legitimate function_call responses for normalized custom tools are kept. type xaiClientToolKey struct { namespace string name string toolType string } func (e *XAIExecutor) prepareResponsesRequest(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, stream bool) (*xaiPreparedRequest, error) { return e.prepareResponsesRequestTo(ctx, req, opts, stream, sdktranslator.FormatCodex) } func (e *XAIExecutor) prepareResponsesRequestTo(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, stream bool, to sdktranslator.Format) (*xaiPreparedRequest, error) { baseModel := thinking.ParseSuffix(req.Model).ModelName from := opts.SourceFormat responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts) originalPayloadSource := req.Payload if len(opts.OriginalRequest) > 0 { originalPayloadSource = opts.OriginalRequest } originalPayload := bytes.Clone(originalPayloadSource) originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream) body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream) var err error body, err = thinking.ApplyThinking(body, req.Model, from.String(), e.Identifier(), e.Identifier()) if err != nil { return nil, err } requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers) body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "stream", stream) body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") body, _ = sjson.DeleteBytes(body, "stream_options") namespaceTools := collectXAINamespaceToolRefs(body) // Collect before normalizeXAITools flattens namespace wrappers so keys match // the post-restore (namespace, short-name) shape used by the response filter. clientDeclaredTools := collectXAIClientDeclaredToolKeys(body) body = normalizeXAITools(body) // Drop choices that point at tools removed by normalizeXAITools before we // inject native x_search, so a surviving allowed_tools / forced choice is not // left pointing at a deleted tool once only x_search remains. body = normalizeXAINamespaceToolChoice(body) body = pruneXAIOrphanedToolChoice(body) body = normalizeXAIToolChoiceForTools(body) body = ensureXAINativeXSearchTool(body) var replayScope xaiReasoningReplayScope body, replayScope, err = applyXAIReasoningReplayCacheRequired(ctx, from, req, opts, body) if err != nil { return nil, err } body = normalizeXAIInputCustomToolCalls(body) body = normalizeXAIInputNamespaceToolCalls(body) body = normalizeXAIInputReasoningItems(body) body = sanitizeXAIInputEncryptedContent(body) body = normalizeCodexInstructions(body) body = sanitizeXAIResponsesBody(body, baseModel) sessionID, errSession := xaiResolveComposerSessionID(ctx, req, opts, baseModel) if errSession != nil { return nil, errSession } if sessionID != "" { body, _ = sjson.SetBytes(body, "prompt_cache_key", sessionID) } return &xaiPreparedRequest{ baseModel: baseModel, from: from, responseFormat: responseFormat, to: to, originalPayload: originalPayload, body: body, namespaceTools: namespaceTools, clientDeclaredTools: clientDeclaredTools, sessionID: sessionID, replayScope: replayScope, filterInternalXSearch: xaiRequestHasNativeXSearch(body), }, nil } func (e *XAIExecutor) recordXAIRequest(ctx context.Context, auth *cliproxyauth.Auth, url string, headers http.Header, body []byte) { var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: headers, Body: body, Provider: e.Identifier(), AuthID: authID, AuthLabel: authLabel, AuthType: authType, AuthValue: authValue, }) } func xaiCreds(auth *cliproxyauth.Auth) (token, baseURL string) { if auth == nil { return "", "" } if auth.Attributes != nil { token = strings.TrimSpace(auth.Attributes["api_key"]) baseURL = strings.TrimSpace(auth.Attributes["base_url"]) } if auth.Metadata != nil { if token == "" { token = xaiMetadataString(auth.Metadata, "access_token") } if baseURL == "" { baseURL = xaiMetadataString(auth.Metadata, "base_url") } } return token, baseURL } // xaiUsingAPI reports whether this xAI auth should use the official API path // for non-media HTTP chat. OAuth defaults to false to use Grok Build. func xaiUsingAPI(auth *cliproxyauth.Auth) bool { if auth == nil { return true } if len(auth.Attributes) > 0 { if raw := strings.TrimSpace(auth.Attributes[xaiUsingAPIAttr]); raw != "" { parsed, errParse := strconv.ParseBool(raw) if errParse == nil { return parsed } } } if len(auth.Metadata) > 0 { raw, ok := auth.Metadata[xaiUsingAPIAttr] if ok && raw != nil { switch v := raw.(type) { case bool: return v case string: parsed, errParse := strconv.ParseBool(strings.TrimSpace(v)) if errParse == nil { return parsed } default: } } } if raw := strings.TrimSpace(auth.Attributes["auth_kind"]); raw != "" { return !strings.EqualFold(raw, "oauth") } return !strings.EqualFold(xaiMetadataString(auth.Metadata, "auth_kind"), "oauth") } // xaiChatBaseURL returns the base URL for non-image/video xAI HTTP chat requests. // When auth using_api is true, the official API base URL logic is used. When it // is false (including its OAuth default), empty or official default base_url is // rewritten to the CLI chat-proxy endpoint; an explicit non-default base_url is // still honored. // Websocket transport intentionally does not use this helper: cli-chat-proxy only // accepts HTTP POST and returns 405 for websocket upgrades. func xaiChatBaseURL(auth *cliproxyauth.Auth) string { _, baseURL := xaiCreds(auth) if xaiUsingAPI(auth) { if baseURL == "" { return xaiauth.DefaultAPIBaseURL } return baseURL } if baseURL != "" && !xaiIsDefaultAPIBaseURL(baseURL) { return baseURL } return xaiauth.CLIChatProxyBaseURL } func xaiNormalizeBaseURL(baseURL string) string { return strings.TrimRight(strings.TrimSpace(baseURL), "/") } func xaiIsDefaultAPIBaseURL(baseURL string) bool { return xaiNormalizeBaseURL(baseURL) == xaiNormalizeBaseURL(xaiauth.DefaultAPIBaseURL) } func xaiIsCLIChatProxyBaseURL(baseURL string) bool { return xaiNormalizeBaseURL(baseURL) == xaiNormalizeBaseURL(xaiauth.CLIChatProxyBaseURL) } // xaiBaseURLSource classifies a resolved xAI base URL for logging. func xaiBaseURLSource(baseURL string) string { switch { case xaiIsDefaultAPIBaseURL(baseURL): return "DefaultAPIBaseURL" case xaiIsCLIChatProxyBaseURL(baseURL): return "CLIChatProxyBaseURL" default: return "custom" } } // logXAIResolvedBaseURL emits a console log for the resolved upstream base URL. func logXAIResolvedBaseURL(ctx context.Context, baseURL string) { helps.LogWithRequestID(ctx).Infof("xai: using base_url=%s source=%s", baseURL, xaiBaseURLSource(baseURL)) } func applyXAIHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, sessionID string) { applyXAIDefaultHeaders(r, token, stream, sessionID) applyXAICustomHeaders(r, auth) } func applyXAIDefaultHeaders(r *http.Request, token string, stream bool, sessionID string) { r.Header.Set("Content-Type", "application/json") if strings.TrimSpace(token) != "" { r.Header.Set("Authorization", "Bearer "+token) } if stream { r.Header.Set("Accept", "text/event-stream") } else { r.Header.Set("Accept", "application/json") } r.Header.Set("Connection", "Keep-Alive") if sessionID != "" { r.Header.Set("x-grok-conv-id", sessionID) } } func applyXAICustomHeaders(r *http.Request, auth *cliproxyauth.Auth) { var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs) } // applyXAIChatHeaders applies standard xAI headers for non-image/video chat // requests. When using_api is true, this matches the standard // applyXAIHeaders behavior. CLI chat-proxy identity headers are only attached // when using_api is false and the resolved chat base URL is the official CLI // chat-proxy endpoint. func applyXAIChatHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, sessionID string) { if xaiUsingAPI(auth) { applyXAIHeaders(r, auth, token, stream, sessionID) return } applyXAIDefaultHeaders(r, token, stream, sessionID) if xaiIsCLIChatProxyBaseURL(xaiChatBaseURL(auth)) { r.Header.Set(xaiTokenAuthHeader, xaiTokenAuthValue) r.Header.Set(xaiClientVersionHeader, xaiClientVersionValue) r.Header.Set("User-Agent", "xai-grok-workspace/"+xaiClientVersionValue) } applyXAICustomHeaders(r, auth) } func xaiResolveComposerSessionID(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, baseModel string) (string, error) { if sessionID := xaiExecutionSessionID(req, opts); sessionID != "" { return sessionID, nil } if !xaiRequiresIsolatedConversation(baseModel) { return "", nil } cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, req.Model, req.Payload, opts.Headers) if errCache != nil { return "", errCache } if ok { return cached.ID, nil } return uuid.NewString(), nil } func xaiExecutionSessionID(req cliproxyexecutor.Request, opts cliproxyexecutor.Options) string { if value := xaiMetadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); value != "" { return value } if value := xaiMetadataString(req.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); value != "" { return value } if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() { return strings.TrimSpace(promptCacheKey.String()) } return "" } func xaiRequiresIsolatedConversation(model string) bool { return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), xaiComposerModelPrefix) } func xaiImageEndpointPath(opts cliproxyexecutor.Options) string { if opts.SourceFormat.String() != xaiImageHandlerType { return "" } path := xaiMetadataString(opts.Metadata, cliproxyexecutor.RequestPathMetadataKey) if strings.HasSuffix(path, "/images/edits") { return xaiImagesEditsPath } if strings.HasSuffix(path, "/images/generations") { return xaiImagesGenerationsPath } return xaiDefaultImageEndpointPath } func xaiIsVideoRequest(opts cliproxyexecutor.Options) bool { return opts.SourceFormat.String() == xaiVideoHandlerType } func xaiVideoEndpointPath(opts cliproxyexecutor.Options) string { if !xaiIsVideoRequest(opts) { return "" } path := xaiMetadataString(opts.Metadata, cliproxyexecutor.RequestPathMetadataKey) if strings.HasSuffix(path, "/videos/edits") { return xaiVideosEditsPath } if strings.HasSuffix(path, "/videos/extensions") { return xaiVideosExtensionsPath } if strings.HasSuffix(path, "/videos/generations") { return xaiVideosGenerationsPath } return "" } func xaiMetadataString(meta map[string]any, key string) string { if len(meta) == 0 || key == "" { return "" } value, ok := meta[key] if !ok || value == nil { return "" } switch typed := value.(type) { case string: return strings.TrimSpace(typed) case fmt.Stringer: return strings.TrimSpace(typed.String()) default: return strings.TrimSpace(fmt.Sprint(typed)) } } func sanitizeXAIResponsesBody(body []byte, model string) []byte { if !xaiSupportsReasoningEffort(model) { if gjson.GetBytes(body, "reasoning.effort").Exists() { log.Debugf("xai: stripping reasoning.effort for model %s (no thinking levels in model registry)", model) } body, _ = sjson.DeleteBytes(body, "reasoning.effort") if reasoning := gjson.GetBytes(body, "reasoning"); reasoning.Exists() && reasoning.IsObject() && len(reasoning.Map()) == 0 { body, _ = sjson.DeleteBytes(body, "reasoning") } } return body } // ensureXAINativeXSearchTool appends {"type":"x_search"} when the final tools // list does not already include native X Search. When tool_choice restricts the // model to allowed_tools, x_search is also added there (without duplicates) so // Grok can select the injected tool. HTTP and websocket executors both prepare // payloads through prepareResponsesRequestTo, so this runs once before the body // is submitted upstream. func ensureXAINativeXSearchTool(body []byte) []byte { if !gjson.ValidBytes(body) { return body } if !xaiRequestHasNativeXSearch(body) { tools := gjson.GetBytes(body, "tools") if !tools.Exists() || !tools.IsArray() { body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"x_search"}]`)) } else { body, _ = sjson.SetRawBytes(body, "tools.-1", xaiXSearchToolJSON) } } return ensureXAINativeXSearchAllowedTools(body) } // ensureXAINativeXSearchAllowedTools appends x_search to tool_choice.tools when // the choice mode is allowed_tools and x_search is not already listed. func ensureXAINativeXSearchAllowedTools(body []byte) []byte { choice := gjson.GetBytes(body, "tool_choice") if !choice.IsObject() || choice.Get("type").String() != "allowed_tools" { return body } allowed := choice.Get("tools") if !allowed.Exists() || !allowed.IsArray() { body, _ = sjson.SetRawBytes(body, "tool_choice.tools", []byte(`[{"type":"x_search"}]`)) return body } for _, tool := range allowed.Array() { if strings.TrimSpace(tool.Get("type").String()) == xaiXSearchToolType { return body } } body, _ = sjson.SetRawBytes(body, "tool_choice.tools.-1", xaiXSearchToolJSON) return body } // pruneXAIOrphanedToolChoice removes tool_choice entries that no longer match // any remaining tool after normalizeXAITools filtering. Forced choices that // reference a deleted tool are dropped entirely; allowed_tools lists keep only // choices that still resolve against the post-normalization tools set. func pruneXAIOrphanedToolChoice(body []byte) []byte { if !gjson.ValidBytes(body) { return body } choice := gjson.GetBytes(body, "tool_choice") if !choice.Exists() { return body } available := collectXAIAvailableToolChoiceKeys(body) if choice.Type == gjson.String { // auto / none / required are not tool references. return body } if !choice.IsObject() { return body } choiceType := strings.TrimSpace(choice.Get("type").String()) switch choiceType { case "allowed_tools": return pruneXAIAllowedToolsChoice(body, available) default: if choiceType == "" { return body } if xaiToolChoiceMatchesAvailable(choice, available) { return body } body, _ = sjson.DeleteBytes(body, "tool_choice") return body } } func pruneXAIAllowedToolsChoice(body []byte, available map[xaiToolChoiceKey]struct{}) []byte { allowed := gjson.GetBytes(body, "tool_choice.tools") if !allowed.Exists() || !allowed.IsArray() { body, _ = sjson.DeleteBytes(body, "tool_choice") return body } filtered := []byte(`[]`) changed := false for _, tool := range allowed.Array() { if !xaiToolChoiceMatchesAvailable(tool, available) { changed = true continue } updated, errSet := sjson.SetRawBytes(filtered, "-1", []byte(tool.Raw)) if errSet != nil { return body } filtered = updated } if !changed { return body } if len(gjson.ParseBytes(filtered).Array()) == 0 { body, _ = sjson.DeleteBytes(body, "tool_choice") return body } body, _ = sjson.SetRawBytes(body, "tool_choice.tools", filtered) return body } // xaiToolChoiceKey identifies a selectable tool the way xAI tool_choice entries // reference it after namespace qualification: type alone for host tools, or // type+name for function tools. type xaiToolChoiceKey struct { toolType string name string } func collectXAIAvailableToolChoiceKeys(body []byte) map[xaiToolChoiceKey]struct{} { keys := make(map[xaiToolChoiceKey]struct{}) collect := func(tools gjson.Result) { if !tools.IsArray() { return } for _, tool := range tools.Array() { toolType := strings.TrimSpace(tool.Get("type").String()) if toolType == "" { continue } key := xaiToolChoiceKey{toolType: toolType} if toolType == xaiFunctionToolType || toolType == xaiCustomToolType { key.name = strings.TrimSpace(tool.Get("name").String()) if key.name == "" { continue } } keys[key] = struct{}{} } } collect(gjson.GetBytes(body, "tools")) input := gjson.GetBytes(body, "input") if input.IsArray() { for _, item := range input.Array() { if item.Get("type").String() == "additional_tools" { collect(item.Get("tools")) } } } return keys } func xaiToolChoiceMatchesAvailable(choice gjson.Result, available map[xaiToolChoiceKey]struct{}) bool { toolType := strings.TrimSpace(choice.Get("type").String()) if toolType == "" { return false } key := xaiToolChoiceKey{toolType: toolType} if toolType == xaiFunctionToolType || toolType == xaiCustomToolType { key.name = strings.TrimSpace(choice.Get("name").String()) if key.name == "" { return false } } _, ok := available[key] return ok } func normalizeXAITools(body []byte) []byte { if !gjson.ValidBytes(body) { return body } original := body normalizeAtPath := func(path string) bool { tools := gjson.GetBytes(body, path) if !tools.Exists() || !tools.IsArray() { return true } filtered, changed, ok := normalizeXAIToolArray(tools) if !ok { return false } if !changed { return true } updated, errSet := sjson.SetRawBytes(body, path, filtered) if errSet != nil { return false } body = updated return true } if !normalizeAtPath("tools") { return original } input := gjson.GetBytes(body, "input") if input.Exists() && input.IsArray() { for index, item := range input.Array() { if item.Get("type").String() != "additional_tools" { continue } if !normalizeAtPath(fmt.Sprintf("input.%d.tools", index)) { return original } } } return body } func normalizeXAIToolArray(tools gjson.Result) ([]byte, bool, bool) { changed := false filtered := []byte(`[]`) for _, tool := range tools.Array() { toolType := tool.Get("type").String() if toolType == xaiNamespaceToolType { changed = true namespaceName := tool.Get("name").String() if namespaceTools := tool.Get("tools"); namespaceTools.IsArray() { for _, nestedTool := range namespaceTools.Array() { nestedRaw, nestedChanged, ok := normalizeXAITool(nestedTool, namespaceName) if !ok { return nil, false, false } changed = changed || nestedChanged if len(nestedRaw) == 0 { continue } updated, errSet := sjson.SetRawBytes(filtered, "-1", nestedRaw) if errSet != nil { return nil, false, false } filtered = updated } } continue } raw, toolChanged, ok := normalizeXAITool(tool, "") if !ok { return nil, false, false } changed = changed || toolChanged if len(raw) == 0 { continue } updated, errSet := sjson.SetRawBytes(filtered, "-1", raw) if errSet != nil { return nil, false, false } filtered = updated } return filtered, changed, true } // normalizeXAIToolChoiceForTools drops tool_choice and parallel_tool_calls // when tools are absent or empty (including after normalizeXAITools filtering). // xAI rejects payloads that include tool_choice without any tools defined. // Existence checks avoid unnecessary sjson parse/copy passes. func normalizeXAIToolChoiceForTools(body []byte) []byte { tools := gjson.GetBytes(body, "tools") hasTools := tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 if !hasTools { input := gjson.GetBytes(body, "input") if input.Exists() && input.IsArray() { for _, item := range input.Array() { additionalTools := item.Get("tools") if item.Get("type").String() == "additional_tools" && additionalTools.IsArray() && len(additionalTools.Array()) > 0 { hasTools = true break } } } } if hasTools { return body } if tools.Exists() { body, _ = sjson.DeleteBytes(body, "tools") } if gjson.GetBytes(body, "tool_choice").Exists() { body, _ = sjson.DeleteBytes(body, "tool_choice") } if gjson.GetBytes(body, "parallel_tool_calls").Exists() { body, _ = sjson.DeleteBytes(body, "parallel_tool_calls") } return body } // normalizeXAINamespaceToolChoice qualifies namespaced function choices using // the same names sent in the flattened tools list. xAI does not accept the // Responses namespace field on tool choices. func normalizeXAINamespaceToolChoice(body []byte) []byte { if !gjson.ValidBytes(body) { return body } original := body normalizeAtPath := func(path string) bool { toolChoice := gjson.GetBytes(body, path) if !toolChoice.IsObject() || toolChoice.Get("type").String() != xaiFunctionToolType { return true } namespaceName := strings.TrimSpace(toolChoice.Get("namespace").String()) toolName := strings.TrimSpace(toolChoice.Get("name").String()) qualifiedName := qualifyXAINamespaceToolName(namespaceName, toolName) if namespaceName == "" || qualifiedName == "" { return true } updated, errSet := sjson.SetBytes(body, path+".name", qualifiedName) if errSet != nil { return false } updated, errDelete := sjson.DeleteBytes(updated, path+".namespace") if errDelete != nil { return false } body = updated return true } if !normalizeAtPath("tool_choice") { return original } tools := gjson.GetBytes(body, "tool_choice.tools") if tools.IsArray() { for index := range tools.Array() { if !normalizeAtPath(fmt.Sprintf("tool_choice.tools.%d", index)) { return original } } } return body } func normalizeXAITool(tool gjson.Result, namespaceName string) ([]byte, bool, bool) { toolType := tool.Get("type").String() changed := false if toolType == xaiToolSearchType || toolType == xaiImageGenerationToolType { return nil, true, true } raw := []byte(tool.Raw) if toolType == xaiCustomToolType { if tool.Get("name").String() == "apply_patch" { return nil, true, true } updatedTool, errSet := sjson.SetBytes(raw, "type", xaiFunctionToolType) if errSet != nil { return nil, false, false } raw = updatedTool toolType = xaiFunctionToolType changed = true } if toolType == xaiWebSearchToolType && tool.Get("external_web_access").Exists() { updatedTool, errDel := sjson.DeleteBytes(raw, "external_web_access") if errDel != nil { return nil, false, false } raw = updatedTool changed = true } if toolType == xaiFunctionToolType && !tool.Get("parameters").Exists() { updatedTool, errSet := sjson.SetRawBytes(raw, "parameters", []byte(`{"type":"object","properties":{}}`)) if errSet != nil { return nil, false, false } raw = updatedTool changed = true } // Simplify the Codex Desktop automation schema and root unions that xAI // rejects because function parameters must resolve exclusively to objects. if toolType == xaiFunctionToolType && xaiFunctionParametersNeedSimplification(tool, namespaceName) { updatedTool, errSet := sjson.SetRawBytes(raw, "parameters", []byte(xaiSafeFunctionParameters)) if errSet != nil { return nil, false, false } raw = updatedTool if strict := tool.Get("strict"); strict.Exists() && strict.Bool() { updatedTool, errSet = sjson.SetBytes(raw, "strict", false) if errSet != nil { return nil, false, false } raw = updatedTool } changed = true log.Debugf("xai: simplified parameters for tool %s.%s to avoid upstream schema rejection or hang", namespaceName, tool.Get("name").String()) } if toolType == xaiFunctionToolType && strings.TrimSpace(namespaceName) != "" { qualifiedName := qualifyXAINamespaceToolName(namespaceName, tool.Get("name").String()) if qualifiedName == "" { return nil, false, false } updatedTool, errSet := sjson.SetBytes(raw, "name", qualifiedName) if errSet != nil { return nil, false, false } raw = updatedTool changed = true } return raw, changed, true } func qualifyXAINamespaceToolName(namespaceName, toolName string) string { namespaceName = strings.TrimSpace(namespaceName) toolName = strings.TrimSpace(toolName) if namespaceName == "" || toolName == "" || strings.HasPrefix(toolName, "mcp__") { return toolName } prefix := namespaceName if !strings.HasSuffix(prefix, "__") { prefix += "__" } if strings.HasPrefix(toolName, prefix) { return toolName } return prefix + toolName } func collectXAINamespaceToolRefs(body []byte) map[string]xaiNamespaceToolRef { refs := make(map[string]xaiNamespaceToolRef) collect := func(tools gjson.Result) { if !tools.Exists() || !tools.IsArray() { return } for _, tool := range tools.Array() { if tool.Get("type").String() != xaiNamespaceToolType { continue } namespaceName := strings.TrimSpace(tool.Get("name").String()) if namespaceName == "" { continue } for _, nestedTool := range tool.Get("tools").Array() { toolName := strings.TrimSpace(nestedTool.Get("name").String()) qualifiedName := qualifyXAINamespaceToolName(namespaceName, toolName) if qualifiedName == "" { continue } refs[qualifiedName] = xaiNamespaceToolRef{namespace: namespaceName, name: toolName} } } } collect(gjson.GetBytes(body, "tools")) input := gjson.GetBytes(body, "input") if input.Exists() && input.IsArray() { for _, item := range input.Array() { if item.Get("type").String() == "additional_tools" { collect(item.Get("tools")) } } } return refs } func normalizeXAIInputCustomToolCalls(body []byte) []byte { input := gjson.GetBytes(body, "input") if !input.Exists() || !input.IsArray() { return body } changed := false inputArray := input.Array() items := make([]json.RawMessage, 0, len(inputArray)) for _, item := range inputArray { var normalized []byte switch item.Get("type").String() { case "custom_tool_call": callID := strings.TrimSpace(item.Get("call_id").String()) name := strings.TrimSpace(item.Get("name").String()) if callID == "" || name == "" { changed = true continue } normalized = []byte(`{"type":"function_call"}`) normalized, _ = sjson.SetBytes(normalized, "call_id", callID) normalized, _ = sjson.SetBytes(normalized, "name", name) normalized, _ = sjson.SetBytes(normalized, "arguments", xaiCustomToolCallArguments(item.Get("input"))) case "custom_tool_call_output": callID := strings.TrimSpace(item.Get("call_id").String()) if callID == "" { changed = true continue } normalized = []byte(`{"type":"function_call_output"}`) normalized, _ = sjson.SetBytes(normalized, "call_id", callID) normalized, _ = sjson.SetBytes(normalized, "output", xaiCustomToolCallOutput(item.Get("output"))) default: items = append(items, json.RawMessage(item.Raw)) continue } items = append(items, json.RawMessage(normalized)) changed = true } if !changed { return body } rawInput, errMarshal := json.Marshal(items) if errMarshal != nil { return body } updated, errSet := sjson.SetRawBytes(body, "input", rawInput) if errSet != nil { return body } return updated } func xaiCustomToolCallArguments(input gjson.Result) string { if !input.Exists() { return "{}" } if input.Type == gjson.String { text := input.String() trimmed := strings.TrimSpace(text) if gjson.Valid(trimmed) { parsed := gjson.Parse(trimmed) if parsed.IsObject() { return parsed.Raw } } encoded, errMarshal := json.Marshal(text) if errMarshal != nil { return "{}" } return `{"input":` + string(encoded) + `}` } if input.IsObject() { return input.Raw } if input.Raw != "" { return `{"input":` + input.Raw + `}` } return "{}" } func xaiCustomToolCallOutput(output gjson.Result) string { if !output.Exists() { return "" } if output.Type == gjson.String { return output.String() } return output.Raw } // xAI executes these x_search subtools server-side but exposes their trace as // client-style tool calls. Hide the trace so Responses clients do not execute it again. type xaiInternalXSearchResponseFilter struct { enabled bool clientDeclaredTools map[xaiClientToolKey]struct{} droppedOutputIndexes map[int64]struct{} droppedItemIDs map[string]struct{} } func newXAIInternalXSearchResponseFilter(enabled bool, clientDeclaredTools map[xaiClientToolKey]struct{}) *xaiInternalXSearchResponseFilter { filter := &xaiInternalXSearchResponseFilter{ enabled: enabled, clientDeclaredTools: clientDeclaredTools, } if enabled { filter.droppedOutputIndexes = make(map[int64]struct{}) filter.droppedItemIDs = make(map[string]struct{}) } return filter } func xaiRequestHasNativeXSearch(body []byte) bool { if gjson.GetBytes(body, `tools.#(type=="x_search")`).Exists() { return true } // Multipath queries return an array of matches; an empty array still Exists(). // Check the match count instead of Exists() for additional_tools injection. return len(gjson.GetBytes(body, `input.#(type=="additional_tools")#.tools.#(type=="x_search")`).Array()) > 0 } // collectXAIClientDeclaredToolKeys records client-declared function/custom tools // using the Responses post-restore identity (short name + optional namespace) and // the effective upstream tool type after normalizeXAITool. Client custom tools // are normalized to function before being sent to xAI, so keys use function for // both declaration kinds. Must run before normalizeXAITools flattens namespace wrappers. func collectXAIClientDeclaredToolKeys(body []byte) map[xaiClientToolKey]struct{} { keys := make(map[xaiClientToolKey]struct{}) collect := func(tools gjson.Result) { if !tools.Exists() || !tools.IsArray() { return } for _, tool := range tools.Array() { switch toolType := strings.TrimSpace(tool.Get("type").String()); toolType { case xaiNamespaceToolType: namespaceName := strings.TrimSpace(tool.Get("name").String()) if namespaceName == "" { continue } for _, nestedTool := range tool.Get("tools").Array() { nestedType := strings.TrimSpace(nestedTool.Get("type").String()) if nestedType != xaiFunctionToolType && nestedType != xaiCustomToolType { continue } toolName := strings.TrimSpace(nestedTool.Get("name").String()) if toolName == "" { continue } // normalizeXAITool converts custom → function before upstream send. keys[xaiClientToolKey{namespace: namespaceName, name: toolName, toolType: xaiEffectiveDeclaredToolType(nestedType)}] = struct{}{} } case xaiFunctionToolType, xaiCustomToolType: toolName := strings.TrimSpace(tool.Get("name").String()) if toolName == "" { continue } // normalizeXAITool converts custom → function before upstream send. keys[xaiClientToolKey{namespace: "", name: toolName, toolType: xaiEffectiveDeclaredToolType(toolType)}] = struct{}{} } } } collect(gjson.GetBytes(body, "tools")) input := gjson.GetBytes(body, "input") if input.Exists() && input.IsArray() { for _, item := range input.Array() { if item.Get("type").String() == "additional_tools" { collect(item.Get("tools")) } } } return keys } // xaiEffectiveDeclaredToolType returns the tool type actually sent upstream // after normalizeXAITool. Client custom tools are rewritten to function. func xaiEffectiveDeclaredToolType(toolType string) string { if strings.TrimSpace(toolType) == xaiCustomToolType { return xaiFunctionToolType } return strings.TrimSpace(toolType) } func xaiIsInternalXSearchToolName(name string) bool { switch strings.TrimSpace(name) { case "x_user_search", "x_semantic_search", "x_keyword_search", "x_thread_fetch": return true default: return false } } // xaiResponseCallDeclaredType maps a Responses output call type to the effective // upstream tool declaration kind used when matching client-declared tools. // Client custom tools are normalized to function before upstream send, so only // function_call can match a client-declared same-name tool; custom_tool_call // remains the internal X Search trace shape. func xaiResponseCallDeclaredType(itemType string) string { switch strings.TrimSpace(itemType) { case "function_call": return xaiFunctionToolType case "custom_tool_call": return xaiCustomToolType default: return "" } } // xaiIsInternalXSearchCallID reports whether call_id matches the evidenced xAI // X Search server-side trace prefix (xs_call...), as observed in Responses traffic // for native x_search subtools (see issue #4282 / PR #4284 fixtures). func xaiIsInternalXSearchCallID(callID string) bool { return strings.HasPrefix(strings.TrimSpace(callID), "xs_call") } // xaiIsInternalXSearchCall reports whether an output item is an xAI server-side // X Search subtool trace that should be hidden from Responses clients. // // Evidence from xAI Responses traffic (issue #4282 / PR #4284): // - native x_search subtools are emitted as custom_tool_call items named // x_user_search / x_semantic_search / x_keyword_search / x_thread_fetch // - those traces commonly use call_id values prefixed with "xs_call" // // Client tools that share a short name are preserved only when the response call // kind matches the effective upstream declaration type. Because normalizeXAITool // rewrites client custom → function, a client custom x_keyword_search is keyed as // function and therefore preserves function_call while still filtering genuine // internal custom_tool_call / xs_call* traces. Namespaced restored client tools // are never treated as internal. func xaiIsInternalXSearchCall(item gjson.Result, clientDeclaredTools map[xaiClientToolKey]struct{}) bool { itemType := strings.TrimSpace(item.Get("type").String()) declaredType := xaiResponseCallDeclaredType(itemType) if declaredType == "" { return false } name := strings.TrimSpace(item.Get("name").String()) if !xaiIsInternalXSearchToolName(name) { return false } namespace := strings.TrimSpace(item.Get("namespace").String()) // Namespaced calls are restored client tools, never xAI internal X Search traces. if namespace != "" { return false } // Evidenced internal call_id prefix always identifies server-side X Search traces, // even when a client tool reuses the same short name. if xaiIsInternalXSearchCallID(item.Get("call_id").String()) { return true } // Preserve only client tools whose effective upstream declaration kind matches // this call type (function_call ↔ function after custom normalization). if _, declared := clientDeclaredTools[xaiClientToolKey{namespace: namespace, name: name, toolType: declaredType}]; declared { return false } return true } func (f *xaiInternalXSearchResponseFilter) apply(eventData []byte) []byte { if f == nil || !f.enabled || len(eventData) == 0 || !gjson.ValidBytes(eventData) { return eventData } if item := gjson.GetBytes(eventData, "item"); xaiIsInternalXSearchCall(item, f.clientDeclaredTools) { f.recordDroppedItem(eventData, item) return nil } eventData = f.filterCompletedOutput(eventData) if f.referencesDroppedItem(eventData) { return nil } return f.compactOutputIndex(eventData) } func (f *xaiInternalXSearchResponseFilter) recordDroppedItem(eventData []byte, item gjson.Result) { if outputIndex := gjson.GetBytes(eventData, "output_index"); outputIndex.Exists() { f.droppedOutputIndexes[outputIndex.Int()] = struct{}{} } for _, path := range []string{"id", "call_id"} { if id := strings.TrimSpace(item.Get(path).String()); id != "" { f.droppedItemIDs[id] = struct{}{} } } } func (f *xaiInternalXSearchResponseFilter) referencesDroppedItem(eventData []byte) bool { if outputIndex := gjson.GetBytes(eventData, "output_index"); outputIndex.Exists() { if _, dropped := f.droppedOutputIndexes[outputIndex.Int()]; dropped { return true } } for _, path := range []string{"item_id", "call_id"} { id := strings.TrimSpace(gjson.GetBytes(eventData, path).String()) if _, dropped := f.droppedItemIDs[id]; id != "" && dropped { return true } } return false } func (f *xaiInternalXSearchResponseFilter) compactOutputIndex(eventData []byte) []byte { outputIndex := gjson.GetBytes(eventData, "output_index") if !outputIndex.Exists() { return eventData } original := outputIndex.Int() removedBefore := int64(0) for dropped := range f.droppedOutputIndexes { if dropped < original { removedBefore++ } } if removedBefore == 0 { return eventData } updated, errSet := sjson.SetBytes(eventData, "output_index", original-removedBefore) if errSet != nil { return eventData } return updated } func (f *xaiInternalXSearchResponseFilter) filterCompletedOutput(eventData []byte) []byte { output := gjson.GetBytes(eventData, "response.output") if !output.IsArray() { return eventData } var clientDeclaredTools map[xaiClientToolKey]struct{} if f != nil { clientDeclaredTools = f.clientDeclaredTools } items := make([]json.RawMessage, 0, len(output.Array())) changed := false for _, item := range output.Array() { if xaiIsInternalXSearchCall(item, clientDeclaredTools) { changed = true continue } items = append(items, json.RawMessage(item.Raw)) } if !changed { return eventData } rawOutput, errMarshal := json.Marshal(items) if errMarshal != nil { return eventData } updated, errSet := sjson.SetRawBytes(eventData, "response.output", rawOutput) if errSet != nil { return eventData } return updated } func normalizeXAIInputNamespaceToolCalls(body []byte) []byte { if !gjson.ValidBytes(body) { return body } input := gjson.GetBytes(body, "input") if !input.Exists() || !input.IsArray() { return body } for index, item := range input.Array() { if item.Get("type").String() != "function_call" { continue } namespaceName := strings.TrimSpace(item.Get("namespace").String()) toolName := strings.TrimSpace(item.Get("name").String()) qualifiedName := qualifyXAINamespaceToolName(namespaceName, toolName) if namespaceName == "" || qualifiedName == "" { continue } namePath := fmt.Sprintf("input.%d.name", index) namespacePath := fmt.Sprintf("input.%d.namespace", index) updated, errSet := sjson.SetBytes(body, namePath, qualifiedName) if errSet != nil { continue } updated, errDelete := sjson.DeleteBytes(updated, namespacePath) if errDelete != nil { continue } body = updated } return body } func restoreXAINamespaceToolCalls(data []byte, refs map[string]xaiNamespaceToolRef) []byte { if len(refs) == 0 || len(data) == 0 || !gjson.ValidBytes(data) { return data } data = restoreXAINamespaceToolCallAtPath(data, "item", refs) output := gjson.GetBytes(data, "response.output") if output.Exists() && output.IsArray() { for index := range output.Array() { data = restoreXAINamespaceToolCallAtPath(data, fmt.Sprintf("response.output.%d", index), refs) } } return data } func restoreXAINamespaceToolCallAtPath(data []byte, path string, refs map[string]xaiNamespaceToolRef) []byte { if gjson.GetBytes(data, path+".type").String() != "function_call" { return data } qualifiedName := strings.TrimSpace(gjson.GetBytes(data, path+".name").String()) ref, ok := refs[qualifiedName] if !ok { return data } updated, errSet := sjson.SetBytes(data, path+".name", ref.name) if errSet != nil { return data } updated, errSet = sjson.SetBytes(updated, path+".namespace", ref.namespace) if errSet != nil { return data } return updated } // xaiFunctionParametersNeedSimplification reports whether a function tool, or // a custom tool normalized to a function, has a schema that xAI cannot accept. func xaiFunctionParametersNeedSimplification(tool gjson.Result, namespaceName string) bool { toolType := strings.TrimSpace(tool.Get("type").String()) isFunction := strings.EqualFold(toolType, xaiFunctionToolType) isNormalizedCustom := strings.EqualFold(toolType, xaiCustomToolType) if !isFunction && !isNormalizedCustom { return false } toolName := strings.TrimSpace(tool.Get("name").String()) qualifiedAutomationName := xaiCodexAppNamespaceName + "__" + xaiAutomationUpdateToolName if isFunction && (strings.EqualFold(toolName, qualifiedAutomationName) || (strings.EqualFold(strings.TrimSpace(namespaceName), xaiCodexAppNamespaceName) && strings.EqualFold(toolName, xaiAutomationUpdateToolName))) { return true } parameters := tool.Get("parameters") for _, unionName := range []string{"anyOf", "oneOf"} { union := parameters.Get(unionName) if !union.IsArray() { continue } for _, branch := range union.Array() { branchType := branch.Get("type") if branchType.Type == gjson.String { if !strings.EqualFold(strings.TrimSpace(branchType.String()), "object") { return true } continue } if !branchType.IsArray() { // Without an explicit object type, the branch may accept non-object // values even when it only declares object-specific keywords. return true } allowedTypes := branchType.Array() if len(allowedTypes) == 0 { return true } for _, allowedType := range allowedTypes { if allowedType.Type != gjson.String || !strings.EqualFold(strings.TrimSpace(allowedType.String()), "object") { return true } } } } return false } func sanitizeXAIInputEncryptedContent(body []byte) []byte { input := gjson.GetBytes(body, "input") if !input.Exists() || !input.IsArray() { return body } items := make([]json.RawMessage, 0, len(input.Array())) changed := false dropCount := 0 firstReason := "" firstItemType := "" for _, item := range input.Array() { itemType := strings.TrimSpace(item.Get("type").String()) if itemType != "reasoning" && itemType != "compaction" { items = append(items, json.RawMessage(item.Raw)) continue } encryptedContent := item.Get("encrypted_content") if !encryptedContent.Exists() { items = append(items, json.RawMessage(item.Raw)) continue } reason := "" switch encryptedContent.Type { case gjson.String: if _, err := signature.InspectGrokEncryptedContent(encryptedContent.String()); err != nil { reason = err.Error() } case gjson.Null: reason = "encrypted_content is null" default: reason = fmt.Sprintf("encrypted_content must be a string, got %s", encryptedContent.Type.String()) } if reason == "" { items = append(items, json.RawMessage(item.Raw)) continue } if itemType == "compaction" { changed = true dropCount++ if firstReason == "" { firstReason = reason firstItemType = itemType } continue } next, err := sjson.DeleteBytes([]byte(item.Raw), "encrypted_content") if err != nil { items = append(items, json.RawMessage(item.Raw)) continue } items = append(items, json.RawMessage(next)) changed = true dropCount++ if firstReason == "" { firstReason = reason firstItemType = itemType } } if !changed { return body } rawInput, err := json.Marshal(items) if err != nil { return body } updated, err := sjson.SetRawBytes(body, "input", rawInput) if err != nil { return body } if dropCount > 0 { log.WithFields(log.Fields{ "component": "xai_encrypted_content_sanitizer", "dropped": dropCount, "first_item_type": firstItemType, "first_reason": firstReason, }).Debug("xai executor: removed invalid encrypted_content before upstream") } return mergeAdjacentXAIInputReasoningSummaries(updated) } func normalizeXAIInputReasoningItems(body []byte) []byte { input := gjson.GetBytes(body, "input") if !input.Exists() || !input.IsArray() { return body } updated := body for i, item := range input.Array() { if item.Get("type").String() != "reasoning" { continue } contentPath := fmt.Sprintf("input.%d.content", i) if content := gjson.GetBytes(updated, contentPath); content.Exists() && content.Type == gjson.Null { updatedBody, errDel := sjson.DeleteBytes(updated, contentPath) if errDel != nil { return body } updated = updatedBody } encryptedContentPath := fmt.Sprintf("input.%d.encrypted_content", i) if encryptedContent := gjson.GetBytes(updated, encryptedContentPath); encryptedContent.Exists() && encryptedContent.Type == gjson.Null { updatedBody, errDel := sjson.DeleteBytes(updated, encryptedContentPath) if errDel != nil { return body } updated = updatedBody } } return mergeAdjacentXAIInputReasoningSummaries(updated) } func mergeAdjacentXAIInputReasoningSummaries(body []byte) []byte { input := gjson.GetBytes(body, "input") if !input.Exists() || !input.IsArray() { return body } changed := false items := make([]json.RawMessage, 0, len(input.Array())) for _, item := range input.Array() { if len(items) > 0 && canMergeXAIReasoningSummary(items[len(items)-1], item) { merged, ok := appendXAIReasoningSummary(items[len(items)-1], item.Get("summary").Array()) if ok { items[len(items)-1] = json.RawMessage(merged) changed = true continue } } items = append(items, json.RawMessage(item.Raw)) } if !changed { return body } rawInput, errMarshal := json.Marshal(items) if errMarshal != nil { return body } updated, errSet := sjson.SetRawBytes(body, "input", rawInput) if errSet != nil { return body } return updated } func canMergeXAIReasoningSummary(previous json.RawMessage, current gjson.Result) bool { previousItem := gjson.ParseBytes(previous) if previousItem.Get("type").String() != "reasoning" || current.Get("type").String() != "reasoning" { return false } if !previousItem.Get("summary").IsArray() || !current.Get("summary").IsArray() { return false } if len(current.Get("summary").Array()) == 0 { return false } for name := range current.Map() { if name != "type" && name != "summary" { return false } } return true } func appendXAIReasoningSummary(previous json.RawMessage, currentSummary []gjson.Result) ([]byte, bool) { updated := []byte(previous) summary := gjson.GetBytes(updated, "summary") if !summary.IsArray() { return previous, false } nextIndex := len(summary.Array()) for i, item := range currentSummary { updatedItem, errSet := sjson.SetRawBytes(updated, fmt.Sprintf("summary.%d", nextIndex+i), []byte(item.Raw)) if errSet != nil { return previous, false } updated = updatedItem } return updated, true } // xaiSupportsReasoningEffort reports whether the model accepts Responses API // reasoning.effort. Capability comes from model registry thinking metadata // (static models.json and dynamic registrations), not a hard-coded name allowlist. func xaiSupportsReasoningEffort(model string) bool { name := strings.ToLower(strings.TrimSpace(thinking.ParseSuffix(model).ModelName)) if idx := strings.LastIndex(name, "/"); idx >= 0 { name = name[idx+1:] } if name == "" { return false } info := registry.LookupModelInfo(name, "xai") if info == nil || info.Thinking == nil { return false } return len(info.Thinking.Levels) > 0 } func xaiNormalizeReasoningSummaryEventLine(line []byte, eventName string) []byte { if eventName == "" && bytes.HasPrefix(line, xaiEventTag) { eventName = strings.TrimSpace(string(line[len(xaiEventTag):])) } eventName = xaiNormalizeReasoningSummaryEventName(eventName) if eventName == "" { return bytes.Clone(line) } return []byte("event: " + eventName) } func xaiNormalizeReasoningSummaryEventName(eventName string) string { switch eventName { case "response.reasoning_text.delta": return "response.reasoning_summary_text.delta" case "response.reasoning_text.done": return "response.reasoning_summary_part.done" default: return eventName } } func xaiNormalizeReasoningSummaryData(eventData []byte) []byte { if len(eventData) == 0 || !gjson.ValidBytes(eventData) { return eventData } normalized := eventData switch gjson.GetBytes(normalized, "type").String() { case "response.reasoning_text.delta": normalized, _ = sjson.SetBytes(normalized, "type", "response.reasoning_summary_text.delta") normalized = xaiNormalizeReasoningSummaryIndex(normalized) case "response.reasoning_text.done": normalized, _ = sjson.SetBytes(normalized, "type", "response.reasoning_summary_part.done") normalized, _ = sjson.SetBytes(normalized, "part.type", "summary_text") if text := gjson.GetBytes(normalized, "text"); text.Exists() { normalized, _ = sjson.SetBytes(normalized, "part.text", text.String()) } normalized, _ = sjson.DeleteBytes(normalized, "text") normalized = xaiNormalizeReasoningSummaryIndex(normalized) case "response.content_part.added": if gjson.GetBytes(normalized, "part.type").String() == "reasoning_text" { normalized, _ = sjson.SetBytes(normalized, "type", "response.reasoning_summary_part.added") normalized, _ = sjson.SetBytes(normalized, "part.type", "summary_text") normalized = xaiNormalizeReasoningSummaryIndex(normalized) } case "response.content_part.done": if gjson.GetBytes(normalized, "part.type").String() == "reasoning_text" { normalized, _ = sjson.SetBytes(normalized, "type", "response.reasoning_summary_part.done") normalized, _ = sjson.SetBytes(normalized, "part.type", "summary_text") normalized = xaiNormalizeReasoningSummaryIndex(normalized) } } if item := gjson.GetBytes(normalized, "item"); item.Exists() && item.Type == gjson.JSON { updatedItem := xaiNormalizeReasoningOutputItem([]byte(item.Raw)) if !bytes.Equal(updatedItem, []byte(item.Raw)) { normalized, _ = sjson.SetRawBytes(normalized, "item", updatedItem) } } if output := gjson.GetBytes(normalized, "response.output"); output.IsArray() { updatedOutput, changed := xaiNormalizeReasoningOutputItems(output.Array()) if changed { normalized, _ = sjson.SetRawBytes(normalized, "response.output", updatedOutput) } } return normalized } func xaiNormalizeReasoningSummaryDataEvents(eventData []byte) [][]byte { if len(eventData) == 0 || !gjson.ValidBytes(eventData) { return [][]byte{eventData} } if gjson.GetBytes(eventData, "type").String() != "response.reasoning_text.done" { return [][]byte{xaiNormalizeReasoningSummaryData(eventData)} } textDone, _ := sjson.SetBytes(eventData, "type", "response.reasoning_summary_text.done") textDone = xaiNormalizeReasoningSummaryIndex(textDone) partDone := xaiNormalizeReasoningSummaryData(eventData) return [][]byte{textDone, partDone} } func xaiNormalizeReasoningSummaryIndex(eventData []byte) []byte { contentIndex := gjson.GetBytes(eventData, "content_index") if contentIndex.Exists() && contentIndex.Raw != "" && !gjson.GetBytes(eventData, "summary_index").Exists() { eventData, _ = sjson.SetRawBytes(eventData, "summary_index", []byte(contentIndex.Raw)) } eventData, _ = sjson.DeleteBytes(eventData, "content_index") return eventData } func xaiNormalizeReasoningOutputItems(items []gjson.Result) ([]byte, bool) { var buf bytes.Buffer buf.WriteByte('[') changed := false for i, item := range items { if i > 0 { buf.WriteByte(',') } updatedItem := xaiNormalizeReasoningOutputItem([]byte(item.Raw)) if !bytes.Equal(updatedItem, []byte(item.Raw)) { changed = true } buf.Write(updatedItem) } buf.WriteByte(']') return buf.Bytes(), changed } func xaiNormalizeReasoningOutputItem(item []byte) []byte { if !gjson.ValidBytes(item) || gjson.GetBytes(item, "type").String() != "reasoning" { return item } normalized := item if summary := gjson.GetBytes(normalized, "summary"); summary.IsArray() { updatedSummary, changed := xaiNormalizeReasoningSummaryItems(summary.Array()) if changed { normalized, _ = sjson.SetRawBytes(normalized, "summary", updatedSummary) } } content := gjson.GetBytes(normalized, "content") if !content.IsArray() { return normalized } summaryItems := make([]gjson.Result, 0, len(content.Array())) for _, part := range content.Array() { if part.Get("type").String() == "reasoning_text" { summaryItems = append(summaryItems, part) } } if len(summaryItems) == 0 { return normalized } updatedSummary, _ := xaiNormalizeReasoningSummaryItems(summaryItems) normalized, _ = sjson.SetRawBytes(normalized, "summary", updatedSummary) normalized, _ = sjson.DeleteBytes(normalized, "content") return normalized } func xaiNormalizeReasoningSummaryItems(items []gjson.Result) ([]byte, bool) { var buf bytes.Buffer buf.WriteByte('[') changed := false for i, item := range items { if i > 0 { buf.WriteByte(',') } itemRaw := []byte(item.Raw) if item.Get("type").String() == "reasoning_text" { var errSet error itemRaw, errSet = sjson.SetBytes(itemRaw, "type", "summary_text") if errSet == nil { changed = true } } buf.Write(itemRaw) } buf.WriteByte(']') return buf.Bytes(), changed } func xaiCollectOutputItemDone(eventData []byte, outputItemsByIndex map[int64][]byte, outputItemsFallback *[][]byte) { itemResult := gjson.GetBytes(eventData, "item") if !itemResult.Exists() || itemResult.Type != gjson.JSON { return } outputIndexResult := gjson.GetBytes(eventData, "output_index") if outputIndexResult.Exists() { outputItemsByIndex[outputIndexResult.Int()] = []byte(itemResult.Raw) return } *outputItemsFallback = append(*outputItemsFallback, []byte(itemResult.Raw)) } func xaiPatchCompletedOutput(eventData []byte, outputItemsByIndex map[int64][]byte, outputItemsFallback [][]byte) []byte { outputResult := gjson.GetBytes(eventData, "response.output") shouldPatchOutput := (!outputResult.Exists() || !outputResult.IsArray() || len(outputResult.Array()) == 0) && (len(outputItemsByIndex) > 0 || len(outputItemsFallback) > 0) if !shouldPatchOutput { return eventData } indexes := make([]int64, 0, len(outputItemsByIndex)) for idx := range outputItemsByIndex { indexes = append(indexes, idx) } sort.Slice(indexes, func(i, j int) bool { return indexes[i] < indexes[j] }) outputArray := []byte("[]") var buf bytes.Buffer buf.WriteByte('[') wrote := false for _, idx := range indexes { if wrote { buf.WriteByte(',') } buf.Write(outputItemsByIndex[idx]) wrote = true } for _, item := range outputItemsFallback { if wrote { buf.WriteByte(',') } buf.Write(item) wrote = true } buf.WriteByte(']') if wrote { outputArray = buf.Bytes() } patched, _ := sjson.SetRawBytes(eventData, "response.output", outputArray) return patched } // xaiFreeUsageExhaustedCooldown is the free-tier rolling window advertised by // cli-chat-proxy ("Usage resets over a rolling 24-hour window"). const xaiFreeUsageExhaustedCooldown = 24 * time.Hour // xaiStatusErr wraps upstream error bodies so free-tier exhaustion // (subscription:free-usage-exhausted) carries a 24h RetryAfter hint for // auth cooldown / account rotation. Generic 429s stay without an explicit // retry hint so conductor backoff still applies. func xaiStatusErr(code int, body []byte) statusErr { err := statusErr{code: code, msg: string(body)} if code != http.StatusTooManyRequests || len(body) == 0 { return err } codeStr := strings.ToLower(gjson.GetBytes(body, "code").String()) msg := strings.ToLower(gjson.GetBytes(body, "error").String()) if msg == "" { msg = strings.ToLower(string(body)) } if strings.Contains(codeStr, "free-usage-exhausted") || strings.Contains(msg, "free-usage-exhausted") || strings.Contains(msg, "included free usage") { d := xaiFreeUsageExhaustedCooldown err.retryAfter = &d } return err }