package executor import ( "bytes" "context" "fmt" "io" "net/http" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" 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" ) 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, opts, 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, opts.Headers) 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 } eventType := gjson.GetBytes(eventData, "type").String() switch eventType { case "response.output_item.done": xaiCollectOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.completed", "response.incomplete": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } completedData := xaiPatchCompletedOutput(eventData, outputItemsByIndex, outputItemsFallback) completedData = xaiNormalizeReasoningSummaryData(completedData) if eventType == "response.completed" { // A truncated turn carries no replayable terminal state, so only a // completed response may refresh the reasoning replay cache. cacheXAIReasoningReplayFromCompleted(ctx, prepared.replayScope, completedData) } var param any out := sdktranslator.TranslateNonStream(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, completedData, ¶m) if prepared.responseFormat == sdktranslator.FormatOpenAIResponse { out = helps.EnsureResponsesUsageDetails(out) } 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 or response.incomplete"} } 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) if prepared.responseFormat == sdktranslator.FormatOpenAIResponse { out = helps.EnsureResponsesUsageDetails(out) } 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) // Compact must not use xaiChatBaseURL: CLI chat-proxy returns 404 for // /responses/compact and a 404 cools down the whole xAI auth pool. baseURL := xaiCompactBaseURL(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") // Compact deletes tools after prepareResponsesRequestTo, which can now keep // image_generation and rewrite its forced choice to allowed_tools on grok-4.6+. // Drop the leftover selection so compact does not send tool_choice without tools. prepared.body = normalizeXAIToolChoiceForTools(prepared.body) for _, field := range []string{"max_output_tokens", "temperature", "top_p", "top_k", "stop"} { prepared.body, _ = sjson.DeleteBytes(prepared.body, field) } 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 } // Official API / custom compact endpoints use standard API headers, not CLI // chat-proxy identity headers (which applyXAIChatHeaders may still attach for OAuth chat). applyXAIHeaders(httpReq, auth, token, false, prepared.sessionID, opts.Headers) 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") requestModelName := "" if prepared != nil { requestModelName = gjson.GetBytes(prepared.originalPayload, "model").String() if requestModelName == "" { requestModelName = prepared.baseModel } } if requestModelName == "" { requestModelName = gjson.GetBytes(compactData, "model").String() } if requestModelName != "" { createdResponse, _ = sjson.SetBytes(createdResponse, "model", requestModelName) inProgressResponse, _ = sjson.SetBytes(inProgressResponse, "model", requestModelName) } 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) completedPayload = helps.EnsureResponsesUsageDetails(completedPayload) 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 }