feat(thinking): add xAI provider support with reasoning.effort implementation

- Implemented `xAI` provider for thinking configurations with support for reasoning.effort levels.
- Registered `xAI` in available providers and updated relevant APIs for compatibility.
- Added unit tests for `xAI` provider functionality, including fallback logic for unsupported levels.
- Integrated `xAI` with executor handling and ensured conformance with OpenAI-compatible standards.
This commit is contained in:
Luis Pater
2026-05-19 03:09:53 +08:00
parent ad98c9549a
commit bac006e72b
9 changed files with 127 additions and 6 deletions

View File

@@ -8,4 +8,5 @@ import (
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/geminicli"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/kimi"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/openai"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/xai"
)

View File

@@ -487,7 +487,7 @@ func (e *XAIExecutor) prepareResponsesRequest(ctx context.Context, req cliproxye
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
var err error
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
body, err = thinking.ApplyThinking(body, req.Model, from.String(), e.Identifier(), e.Identifier())
if err != nil {
return nil, err
}

View File

@@ -196,6 +196,48 @@ func TestXAIExecutorOmitsUnsupportedReasoningEffort(t *testing.T) {
}
}
func TestXAIExecutorAppliesThinkingSuffix(t *testing.T) {
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var errRead error
gotBody, errRead = io.ReadAll(r.Body)
if errRead != nil {
t.Fatalf("read body: %v", errRead)
}
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":0,\"status\":\"completed\",\"model\":\"grok-4.3\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]}]}}\n\n"))
}))
defer server.Close()
exec := NewXAIExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Provider: "xai",
Attributes: map[string]string{
"base_url": server.URL,
"auth_kind": "oauth",
},
Metadata: map[string]any{"access_token": "xai-token"},
}
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "grok-4.3(low)",
Payload: []byte(`{"model":"grok-4.3","input":"hello"}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatOpenAIResponse,
Stream: false,
})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if got := gjson.GetBytes(gotBody, "model").String(); got != "grok-4.3" {
t.Fatalf("model = %q, want grok-4.3; body=%s", got, string(gotBody))
}
if got := gjson.GetBytes(gotBody, "reasoning.effort").String(); got != "low" {
t.Fatalf("reasoning.effort = %q, want low; body=%s", got, string(gotBody))
}
}
func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -18,6 +18,7 @@ var providerAppliers = map[string]ProviderApplier{
"codex": nil,
"antigravity": nil,
"kimi": nil,
"xai": nil,
}
// GetProviderApplier returns the ProviderApplier for the given provider name.
@@ -62,7 +63,7 @@ func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool {
// - body: Original request body JSON
// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")
// - fromFormat: Source request format (e.g., openai, codex, gemini)
// - toFormat: Target provider format for the request body (gemini, gemini-cli, antigravity, claude, openai, codex, kimi)
// - toFormat: Target provider format for the request body (gemini, gemini-cli, antigravity, claude, openai, codex, kimi, xai)
// - providerKey: Provider identifier used for registry model lookups (may differ from toFormat, e.g., openrouter -> openai)
//
// Returns:
@@ -324,7 +325,7 @@ func extractThinkingConfig(body []byte, provider string) ThinkingConfig {
return extractGeminiConfig(body, provider)
case "openai":
return extractOpenAIConfig(body)
case "codex":
case "codex", "xai":
return extractCodexConfig(body)
case "kimi":
// Kimi uses OpenAI-compatible reasoning_effort format

View File

@@ -0,0 +1,26 @@
// Package xai implements thinking configuration for xAI Grok Responses API models.
//
// xAI models use the OpenAI Responses API compatible reasoning.effort format
// with discrete levels.
package xai
import (
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/codex"
)
// Applier implements thinking.ProviderApplier for xAI models.
type Applier struct {
codex.Applier
}
var _ thinking.ProviderApplier = (*Applier)(nil)
// NewApplier creates a new xAI thinking applier.
func NewApplier() *Applier {
return &Applier{}
}
func init() {
thinking.RegisterProvider("xai", NewApplier())
}

View File

@@ -0,0 +1,51 @@
package xai
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/tidwall/gjson"
)
func TestApplySetsReasoningEffort(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "grok-4.3",
Thinking: &registry.ThinkingSupport{
ZeroAllowed: true,
Levels: []string{"none", "low", "medium", "high"},
},
}
out, err := applier.Apply([]byte(`{"input":"hello"}`), thinking.ThinkingConfig{
Mode: thinking.ModeLevel,
Level: thinking.LevelHigh,
}, modelInfo)
if err != nil {
t.Fatalf("Apply() error = %v", err)
}
if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "high" {
t.Fatalf("reasoning.effort = %q, want high; body=%s", got, string(out))
}
}
func TestApplyNoneFallsBackToLowestLevelWhenDisableUnsupported(t *testing.T) {
applier := NewApplier()
modelInfo := &registry.ModelInfo{
ID: "grok-3-mini",
Thinking: &registry.ThinkingSupport{
Levels: []string{"low", "medium", "high"},
},
}
out, err := applier.Apply([]byte(`{"input":"hello"}`), thinking.ThinkingConfig{
Mode: thinking.ModeNone,
}, modelInfo)
if err != nil {
t.Fatalf("Apply() error = %v", err)
}
if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "low" {
t.Fatalf("reasoning.effort = %q, want low; body=%s", got, string(out))
}
}

View File

@@ -42,7 +42,7 @@ func StripThinkingConfig(body []byte, provider string) []byte {
"reasoning_effort",
"thinking",
}
case "codex":
case "codex", "xai":
paths = []string{"reasoning.effort"}
default:
return body

View File

@@ -1,7 +1,7 @@
// Package thinking provides unified thinking configuration processing.
//
// This package offers a unified interface for parsing, validating, and applying
// thinking configurations across various AI providers (Claude, Gemini, OpenAI, Codex, Antigravity, Kimi).
// thinking configurations across various AI providers (Claude, Gemini, OpenAI, Codex, Antigravity, Kimi, xAI).
package thinking
import "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"

View File

@@ -357,7 +357,7 @@ func isGeminiFamily(provider string) bool {
func isOpenAIFamily(provider string) bool {
switch provider {
case "openai", "openai-response", "codex":
case "openai", "openai-response", "codex", "xai":
return true
default:
return false