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/geminicli"
|
||||||
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/kimi"
|
_ "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/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)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
||||||
|
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func TestXAIExecutorExecuteStreamFiltersToolSearchTool(t *testing.T) {
|
||||||
var gotBody []byte
|
var gotBody []byte
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var providerAppliers = map[string]ProviderApplier{
|
|||||||
"codex": nil,
|
"codex": nil,
|
||||||
"antigravity": nil,
|
"antigravity": nil,
|
||||||
"kimi": nil,
|
"kimi": nil,
|
||||||
|
"xai": nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProviderApplier returns the ProviderApplier for the given provider name.
|
// GetProviderApplier returns the ProviderApplier for the given provider name.
|
||||||
@@ -62,7 +63,7 @@ func IsUserDefinedModel(modelInfo *registry.ModelInfo) bool {
|
|||||||
// - body: Original request body JSON
|
// - body: Original request body JSON
|
||||||
// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")
|
// - model: Model name, optionally with thinking suffix (e.g., "claude-sonnet-4-5(16384)")
|
||||||
// - fromFormat: Source request format (e.g., openai, codex, gemini)
|
// - 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)
|
// - providerKey: Provider identifier used for registry model lookups (may differ from toFormat, e.g., openrouter -> openai)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
@@ -324,7 +325,7 @@ func extractThinkingConfig(body []byte, provider string) ThinkingConfig {
|
|||||||
return extractGeminiConfig(body, provider)
|
return extractGeminiConfig(body, provider)
|
||||||
case "openai":
|
case "openai":
|
||||||
return extractOpenAIConfig(body)
|
return extractOpenAIConfig(body)
|
||||||
case "codex":
|
case "codex", "xai":
|
||||||
return extractCodexConfig(body)
|
return extractCodexConfig(body)
|
||||||
case "kimi":
|
case "kimi":
|
||||||
// Kimi uses OpenAI-compatible reasoning_effort format
|
// Kimi uses OpenAI-compatible reasoning_effort format
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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",
|
"reasoning_effort",
|
||||||
"thinking",
|
"thinking",
|
||||||
}
|
}
|
||||||
case "codex":
|
case "codex", "xai":
|
||||||
paths = []string{"reasoning.effort"}
|
paths = []string{"reasoning.effort"}
|
||||||
default:
|
default:
|
||||||
return body
|
return body
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package thinking provides unified thinking configuration processing.
|
// Package thinking provides unified thinking configuration processing.
|
||||||
//
|
//
|
||||||
// This package offers a unified interface for parsing, validating, and applying
|
// 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
|
package thinking
|
||||||
|
|
||||||
import "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
import "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ func isGeminiFamily(provider string) bool {
|
|||||||
|
|
||||||
func isOpenAIFamily(provider string) bool {
|
func isOpenAIFamily(provider string) bool {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "openai", "openai-response", "codex":
|
case "openai", "openai-response", "codex", "xai":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user