Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions internal/runtime/executor/xai_executor_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// 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)
Expand Down Expand Up @@ -635,21 +637,6 @@ func xaiCompareGrokVersion(a, b xaiGrokVersion) int {
return 0
}

func sanitizeXAIResponsesBody(body []byte, model string) []byte {
// stop is supported by Chat Completions but not by xAI's Responses API.
body, _ = sjson.DeleteBytes(body, "stop")
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
Expand Down
20 changes: 0 additions & 20 deletions internal/runtime/executor/xai_executor_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):]))
Expand Down
99 changes: 78 additions & 21 deletions internal/runtime/executor/xai_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -2971,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
Expand Down Expand Up @@ -3009,31 +3014,83 @@ func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) {
}
}

func TestXAISupportsReasoningEffortUsesModelRegistry(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")
}
levels := &registry.ThinkingSupport{Levels: []string{"low", "medium", "high"}}
tests := []struct {
name string
model string
want bool
name string
model string
suffix string
metadataKey string
thinking *registry.ThinkingSupport
wantEffort string
}{
{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},
{
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: "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: &registry.ThinkingSupport{}, wantEffort: "high",
},
{
name: "home restricts thinking levels", model: "grok-4.5",
metadataKey: "cliproxy.resolved_home_model_info", thinking: &registry.ThinkingSupport{Levels: []string{"low"}}, wantEffort: "low",
},
{name: "local supported fallback", model: "grok-4.5", wantEffort: "high"},
{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) {
if got := xaiSupportsReasoningEffort(tt.model); got != tt.want {
t.Fatalf("xaiSupportsReasoningEffort(%q) = %v, want %v", tt.model, got, tt.want)
}
})
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": wantEffort},
}}
}
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: &registry.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)
}
})
}
}
}

Expand Down
Loading