From 984aee8df05e63424bf4a5e6d8d407ddf0a013a7 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:22:43 +0800 Subject: [PATCH 1/2] feat(executor): enhance sanitizeXAIResponsesBody to utilize resolved thinking support --- .../runtime/executor/xai_executor_request.go | 14 ++-- .../runtime/executor/xai_executor_test.go | 66 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/internal/runtime/executor/xai_executor_request.go b/internal/runtime/executor/xai_executor_request.go index 7eeb96c319b..4336031e61b 100644 --- a/internal/runtime/executor/xai_executor_request.go +++ b/internal/runtime/executor/xai_executor_request.go @@ -132,7 +132,7 @@ func (e *XAIExecutor) prepareResponsesRequestTo(ctx context.Context, req cliprox body = normalizeXAIInputReasoningItems(body) body = sanitizeXAIInputEncryptedContent(body) body = normalizeCodexInstructions(body) - body = sanitizeXAIResponsesBody(body, baseModel) + body = sanitizeXAIResponsesBody(body, baseModel, req) body = normalizeXAIImageRefs(body) sessionID, errSession := xaiResolveComposerSessionID(ctx, req, opts, baseModel) @@ -635,12 +635,18 @@ func xaiCompareGrokVersion(a, b xaiGrokVersion) int { return 0 } -func sanitizeXAIResponsesBody(body []byte, model string) []byte { +func sanitizeXAIResponsesBody(body []byte, model string, req cliproxyexecutor.Request) []byte { // stop is supported by Chat Completions but not by xAI's Responses API. body, _ = sjson.DeleteBytes(body, "stop") - if !xaiSupportsReasoningEffort(model) { + var supportsReasoningEffort bool + if info, ok := cliproxyauth.ResolvedModelInfo(req); ok { + supportsReasoningEffort = info.Thinking != nil && len(info.Thinking.Levels) > 0 + } else { + supportsReasoningEffort = xaiSupportsReasoningEffort(model) + } + if !supportsReasoningEffort { if gjson.GetBytes(body, "reasoning.effort").Exists() { - log.Debugf("xai: stripping reasoning.effort for model %s (no thinking levels in model registry)", model) + log.Debugf("xai: stripping reasoning.effort for model %s (no thinking levels in model capabilities)", model) } body, _ = sjson.DeleteBytes(body, "reasoning.effort") if reasoning := gjson.GetBytes(body, "reasoning"); reasoning.Exists() && reasoning.IsObject() && len(reasoning.Map()) == 0 { diff --git a/internal/runtime/executor/xai_executor_test.go b/internal/runtime/executor/xai_executor_test.go index 03780297654..987e504f72a 100644 --- a/internal/runtime/executor/xai_executor_test.go +++ b/internal/runtime/executor/xai_executor_test.go @@ -20,6 +20,7 @@ import ( xaiauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/xai" internalcache "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" "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/translator" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" @@ -3009,6 +3010,71 @@ func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) { } } +func TestXAIExecutorUsesResolvedThinkingSupport(t *testing.T) { + const remoteModel = "grok-home-only-thinking-test" + if registry.LookupModelInfo(remoteModel, "xai") != nil { + t.Fatal("test model must be absent from the local registry") + } + levels := ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high"}} + tests := []struct { + name string + model string + metadataKey string + thinking *registry.ThinkingSupport + wantEffort string + }{ + { + name: "home-only model", model: remoteModel, + metadataKey: "cliproxy.resolved_home_model_info", thinking: levels, wantEffort: "high", + }, + { + name: "configured API-key model", model: remoteModel, + metadataKey: "cliproxy.resolved_api_key_model_info", thinking: levels, wantEffort: "high", + }, + { + name: "home disables local thinking support", model: "grok-4.5", + metadataKey: "cliproxy.resolved_home_model_info", + }, + { + name: "home supplies no thinking levels", model: "grok-4.5", + metadataKey: "cliproxy.resolved_home_model_info", thinking: ®istry.ThinkingSupport{}, + }, + {name: "local supported fallback", model: "grok-4.5", wantEffort: "high"}, + {name: "local unknown fallback", model: remoteModel}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exec := NewXAIExecutor(&config.Config{ + Payload: config.PayloadConfig{ + // Re-add effort after canonical thinking processing to exercise the final sanitizer. + Override: []config.PayloadRule{{ + Models: []config.PayloadModelRule{{Name: tt.model}}, + Params: map[string]any{"reasoning.effort": "high"}, + }}, + }, + }) + req := cliproxyexecutor.Request{ + Model: tt.model, + Payload: []byte(fmt.Sprintf(`{"model":%q,"input":"hello","reasoning":{"effort":"high"}}`, tt.model)), + } + if tt.metadataKey != "" { + req.Metadata = map[string]any{ + tt.metadataKey: ®istry.ModelInfo{ID: tt.model, Type: "xai", Thinking: tt.thinking}, + } + } + prepared, errPrepare := exec.prepareResponsesRequest(t.Context(), req, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FormatOpenAIResponse, + }, false) + if errPrepare != nil { + t.Fatalf("prepareResponsesRequest() error = %v", errPrepare) + } + if got := gjson.GetBytes(prepared.body, "reasoning.effort").String(); got != tt.wantEffort { + t.Fatalf("reasoning.effort = %q, want %q; body=%s", got, tt.wantEffort, prepared.body) + } + }) + } +} + func TestXAISupportsReasoningEffortUsesModelRegistry(t *testing.T) { tests := []struct { name string From 6d1d74b30d7730335b6f5d90054f5d6211c52b92 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:02:02 +0800 Subject: [PATCH 2/2] refactor(executor): remove unsupported reasoning effort handling and update related tests --- .../runtime/executor/xai_executor_request.go | 25 +--- .../runtime/executor/xai_executor_response.go | 20 ---- .../runtime/executor/xai_executor_test.go | 107 ++++++++---------- 3 files changed, 52 insertions(+), 100 deletions(-) diff --git a/internal/runtime/executor/xai_executor_request.go b/internal/runtime/executor/xai_executor_request.go index 4336031e61b..cad085d39ac 100644 --- a/internal/runtime/executor/xai_executor_request.go +++ b/internal/runtime/executor/xai_executor_request.go @@ -132,7 +132,9 @@ func (e *XAIExecutor) prepareResponsesRequestTo(ctx context.Context, req cliprox body = normalizeXAIInputReasoningItems(body) body = sanitizeXAIInputEncryptedContent(body) body = normalizeCodexInstructions(body) - body = sanitizeXAIResponsesBody(body, baseModel, req) + // stop is supported by Chat Completions but not by xAI's Responses API. + // Thinking was handled before payload overrides and must not be revalidated here. + body, _ = sjson.DeleteBytes(body, "stop") body = normalizeXAIImageRefs(body) sessionID, errSession := xaiResolveComposerSessionID(ctx, req, opts, baseModel) @@ -635,27 +637,6 @@ func xaiCompareGrokVersion(a, b xaiGrokVersion) int { return 0 } -func sanitizeXAIResponsesBody(body []byte, model string, req cliproxyexecutor.Request) []byte { - // stop is supported by Chat Completions but not by xAI's Responses API. - body, _ = sjson.DeleteBytes(body, "stop") - var supportsReasoningEffort bool - if info, ok := cliproxyauth.ResolvedModelInfo(req); ok { - supportsReasoningEffort = info.Thinking != nil && len(info.Thinking.Levels) > 0 - } else { - supportsReasoningEffort = xaiSupportsReasoningEffort(model) - } - if !supportsReasoningEffort { - if gjson.GetBytes(body, "reasoning.effort").Exists() { - log.Debugf("xai: stripping reasoning.effort for model %s (no thinking levels in model capabilities)", 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 diff --git a/internal/runtime/executor/xai_executor_response.go b/internal/runtime/executor/xai_executor_response.go index 633cb8bdaf8..5f19d7dd34a 100644 --- a/internal/runtime/executor/xai_executor_response.go +++ b/internal/runtime/executor/xai_executor_response.go @@ -9,10 +9,8 @@ import ( "strings" "time" - "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" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -862,24 +860,6 @@ func appendXAIReasoningSummary(previous json.RawMessage, currentSummary []gjson. 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):])) diff --git a/internal/runtime/executor/xai_executor_test.go b/internal/runtime/executor/xai_executor_test.go index 987e504f72a..bc621d8a08d 100644 --- a/internal/runtime/executor/xai_executor_test.go +++ b/internal/runtime/executor/xai_executor_test.go @@ -2972,6 +2972,10 @@ func TestXAIExecutorExecuteStreamCompactionTriggerUsesCompactEndpoint(t *testing } func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) { + modelRegistry := registry.GetGlobalRegistry() + modelRegistry.RegisterClient("xai-non-thinking-test", "xai", []*registry.ModelInfo{{ID: "grok-4", Type: "xai"}}) + t.Cleanup(func() { modelRegistry.UnregisterClient("xai-non-thinking-test") }) + var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var errRead error @@ -3010,7 +3014,7 @@ func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) { } } -func TestXAIExecutorUsesResolvedThinkingSupport(t *testing.T) { +func TestXAIExecutorThinkingPayloadOverride(t *testing.T) { const remoteModel = "grok-home-only-thinking-test" if registry.LookupModelInfo(remoteModel, "xai") != nil { t.Fatal("test model must be absent from the local registry") @@ -3019,6 +3023,7 @@ func TestXAIExecutorUsesResolvedThinkingSupport(t *testing.T) { tests := []struct { name string model string + suffix string metadataKey string thinking *registry.ThinkingSupport wantEffort string @@ -3035,71 +3040,57 @@ func TestXAIExecutorUsesResolvedThinkingSupport(t *testing.T) { name: "home disables local thinking support", model: "grok-4.5", metadataKey: "cliproxy.resolved_home_model_info", }, + { + name: "API-key disables local thinking support", model: "grok-4.5", + metadataKey: "cliproxy.resolved_api_key_model_info", + }, { name: "home supplies no thinking levels", model: "grok-4.5", - metadataKey: "cliproxy.resolved_home_model_info", thinking: ®istry.ThinkingSupport{}, + metadataKey: "cliproxy.resolved_home_model_info", thinking: ®istry.ThinkingSupport{}, wantEffort: "high", + }, + { + name: "home restricts thinking levels", model: "grok-4.5", + metadataKey: "cliproxy.resolved_home_model_info", thinking: ®istry.ThinkingSupport{Levels: []string{"low"}}, wantEffort: "low", }, {name: "local supported fallback", model: "grok-4.5", wantEffort: "high"}, - {name: "local unknown fallback", model: remoteModel}, + {name: "local unsupported fallback", model: "grok-build-0.1"}, + {name: "local unknown fallback", model: remoteModel, wantEffort: "high"}, + {name: "model suffix", model: "grok-4.5", suffix: "(low)", wantEffort: "low"}, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - exec := NewXAIExecutor(&config.Config{ - Payload: config.PayloadConfig{ - // Re-add effort after canonical thinking processing to exercise the final sanitizer. - Override: []config.PayloadRule{{ + for _, override := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/override=%t", tt.name, override), func(t *testing.T) { + cfg := &config.Config{} + wantEffort := tt.wantEffort + if override { + // Explicit overrides may force an effort beyond the model's declared capabilities. + wantEffort = "xhigh" + cfg.Payload.Override = []config.PayloadRule{{ Models: []config.PayloadModelRule{{Name: tt.model}}, - Params: map[string]any{"reasoning.effort": "high"}, - }}, - }, - }) - req := cliproxyexecutor.Request{ - Model: tt.model, - Payload: []byte(fmt.Sprintf(`{"model":%q,"input":"hello","reasoning":{"effort":"high"}}`, tt.model)), - } - if tt.metadataKey != "" { - req.Metadata = map[string]any{ - tt.metadataKey: ®istry.ModelInfo{ID: tt.model, Type: "xai", Thinking: tt.thinking}, + Params: map[string]any{"reasoning.effort": wantEffort}, + }} } - } - prepared, errPrepare := exec.prepareResponsesRequest(t.Context(), req, cliproxyexecutor.Options{ - SourceFormat: sdktranslator.FormatOpenAIResponse, - }, false) - if errPrepare != nil { - t.Fatalf("prepareResponsesRequest() error = %v", errPrepare) - } - if got := gjson.GetBytes(prepared.body, "reasoning.effort").String(); got != tt.wantEffort { - t.Fatalf("reasoning.effort = %q, want %q; body=%s", got, tt.wantEffort, prepared.body) - } - }) - } -} - -func TestXAISupportsReasoningEffortUsesModelRegistry(t *testing.T) { - tests := []struct { - name string - model string - want bool - }{ - {name: "grok-4.5", model: "grok-4.5", want: true}, - {name: "grok-4.5 with suffix", model: "grok-4.5(high)", want: true}, - {name: "grok-4.3", model: "grok-4.3", want: true}, - {name: "grok-3-mini", model: "grok-3-mini", want: true}, - {name: "grok-3-mini-fast", model: "grok-3-mini-fast", want: true}, - {name: "grok-4.20-multi-agent", model: "grok-4.20-multi-agent-0309", want: true}, - {name: "provider-prefixed grok-4.5", model: "xai/grok-4.5", want: true}, - {name: "legacy grok-4", model: "grok-4", want: false}, - {name: "composer without thinking metadata", model: "grok-composer-2.5-fast", want: false}, - {name: "non-reasoning 4.20", model: "grok-4.20-0309-non-reasoning", want: false}, - {name: "unknown model", model: "unknown-xai-model", want: false}, - {name: "empty model", model: "", want: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := xaiSupportsReasoningEffort(tt.model); got != tt.want { - t.Fatalf("xaiSupportsReasoningEffort(%q) = %v, want %v", tt.model, got, tt.want) - } - }) + exec := NewXAIExecutor(cfg) + req := cliproxyexecutor.Request{ + Model: tt.model + tt.suffix, + Payload: []byte(fmt.Sprintf(`{"model":%q,"input":"hello","reasoning":{"effort":"high"}}`, tt.model)), + } + if tt.metadataKey != "" { + req.Metadata = map[string]any{ + tt.metadataKey: ®istry.ModelInfo{ID: tt.model, Type: "xai", Thinking: tt.thinking}, + } + } + prepared, errPrepare := exec.prepareResponsesRequest(t.Context(), req, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FormatOpenAIResponse, + }, false) + if errPrepare != nil { + t.Fatalf("prepareResponsesRequest() error = %v", errPrepare) + } + if got := gjson.GetBytes(prepared.body, "reasoning.effort").String(); got != wantEffort { + t.Fatalf("reasoning.effort = %q, want %q; body=%s", got, wantEffort, prepared.body) + } + }) + } } }