mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-05-23 04:39:53 +08:00
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:
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
26
internal/thinking/provider/xai/apply.go
Normal file
26
internal/thinking/provider/xai/apply.go
Normal 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())
|
||||
}
|
||||
51
internal/thinking/provider/xai/apply_test.go
Normal file
51
internal/thinking/provider/xai/apply_test.go
Normal 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 := ®istry.ModelInfo{
|
||||
ID: "grok-4.3",
|
||||
Thinking: ®istry.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 := ®istry.ModelInfo{
|
||||
ID: "grok-3-mini",
|
||||
Thinking: ®istry.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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user