Files
CLIProxyAPI/internal/runtime/executor/xai_executor.go
sususu98 53a21dfb0b [codex] Drop foreign encrypted_content before xAI Grok upstream (#3961)
* Drop foreign encrypted_content before xAI Grok upstream

xAI Grok accepts provider-native encrypted_content as opaque replay state, but GPT/Codex reasoning signatures, Gemini thoughtSignature blobs, and Claude thinking signatures can all travel through OpenAI Responses-style reasoning.encrypted_content while remaining incompatible with xAI. Forwarding those foreign blobs to Grok causes upstream validation failures, especially when the foreign value is high-entropy enough to look ciphertext-like.

Add a Grok encrypted_content transport validator that stays conservative and shape-oriented:

- require unpadded standard base64 with no foreign characters
- reject obvious GPT/Codex gAAAA reasoning signatures before decode
- reject strict Claude thinking signatures in both official E-form and Antigravity R-form
- reject known Gemini thoughtSignature envelopes by reusing the central Gemini validator, covering Gemini 2.5 field-1 and Gemini 3.x field-2 shapes
- require decoded payloads to be long enough and high-entropy enough to look like native Grok ciphertext
- avoid decrypting, protobuf-parsing, or otherwise interpreting native Grok payloads on the hot path

Wire the validator into the xAI Responses request preparation path for reasoning and compaction input items. Invalid encrypted_content fields are deleted before the request is sent upstream, while the surrounding item is preserved and debug logging records only redacted metadata.

Extend coverage with native Grok corpus preservation, Gemini field-1/field-2 rejection, Claude E-form and R-form rejection, invalid-blob sanitizer tests, and compact/websocket replay preservation. The foreign-provider checks are deliberately narrow so high-entropy Grok blobs are not rejected merely because they look random.

* fix(xai): harden encrypted content sanitizer
2026-06-23 14:33:27 +08:00

1574 lines
51 KiB
Go

package executor
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"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/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"
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-"
)
// 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, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
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
}
applyXAIHeaders(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, statusErr{code: httpResp.StatusCode, msg: string(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
for _, line := range bytes.Split(data, []byte("\n")) {
if !bytes.HasPrefix(line, xaiDataTag) {
continue
}
eventData := xaiNormalizeReasoningSummaryData(bytes.TrimSpace(line[len(xaiDataTag):]))
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)
var param any
out := sdktranslator.TranslateNonStream(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, completedData, &param)
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, &param)
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, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
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
}
applyXAIHeaders(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 = statusErr{code: httpResp.StatusCode, msg: string(data)}
return nil, nil, nil, err
}
reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
reporter.EnsurePublished(ctx)
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) {
token, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
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)
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, statusErr{code: httpResp.StatusCode, msg: string(data)}
}
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
}
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, statusErr{code: httpResp.StatusCode, msg: string(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, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
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
}
applyXAIHeaders(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, statusErr{code: httpResp.StatusCode, msg: string(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
var pendingEventLine []byte
emitTranslatedLine := func(translatedLine []byte) bool {
chunks := sdktranslator.TranslateStream(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, translatedLine, &param)
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 {
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)
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
sessionID 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")
body = normalizeXAITools(body)
body = normalizeXAIToolChoiceForTools(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,
sessionID: sessionID,
}, 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
}
func applyXAIHeaders(r *http.Request, auth *cliproxyauth.Auth, 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)
}
var attrs map[string]string
if auth != nil {
attrs = auth.Attributes
}
util.ApplyCustomHeadersFromAttrs(r, attrs)
}
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 {
body = removeXAIEncryptedReasoningInclude(body)
if !xaiSupportsReasoningEffort(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
}
func normalizeXAITools(body []byte) []byte {
tools := gjson.GetBytes(body, "tools")
if !tools.Exists() || !tools.IsArray() {
return body
}
changed := false
filtered := []byte(`[]`)
for _, tool := range tools.Array() {
toolType := tool.Get("type").String()
if toolType == xaiNamespaceToolType {
changed = true
if namespaceTools := tool.Get("tools"); namespaceTools.IsArray() {
for _, nestedTool := range namespaceTools.Array() {
nestedRaw, nestedChanged, ok := normalizeXAITool(nestedTool)
if !ok {
return body
}
changed = changed || nestedChanged
if len(nestedRaw) == 0 {
continue
}
updated, errSet := sjson.SetRawBytes(filtered, "-1", nestedRaw)
if errSet != nil {
return body
}
filtered = updated
}
}
continue
}
raw, toolChanged, ok := normalizeXAITool(tool)
if !ok {
return body
}
changed = changed || toolChanged
if len(raw) == 0 {
continue
}
updated, errSet := sjson.SetRawBytes(filtered, "-1", raw)
if errSet != nil {
return body
}
filtered = updated
}
if !changed {
return body
}
updated, errSet := sjson.SetRawBytes(body, "tools", filtered)
if errSet != nil {
return body
}
return updated
}
// 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 {
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
}
func normalizeXAITool(tool gjson.Result) ([]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
}
return raw, changed, true
}
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
}
func removeXAIEncryptedReasoningInclude(body []byte) []byte {
include := gjson.GetBytes(body, "include")
if !include.Exists() || !include.IsArray() {
return body
}
kept := make([]string, 0, len(include.Array()))
for _, item := range include.Array() {
value := strings.TrimSpace(item.String())
if value == "" || value == "reasoning.encrypted_content" {
continue
}
kept = append(kept, value)
}
body, _ = sjson.SetBytes(body, "include", kept)
return body
}
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:]
}
switch {
case strings.HasPrefix(name, "grok-3-mini"):
return true
case strings.HasPrefix(name, "grok-4.20-multi-agent"):
return true
case strings.HasPrefix(name, "grok-4.3"):
return true
default:
return false
}
}
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
}