feat(executor): add chat-proxy-specific headers and improve base URL handling

- Introduced `xaiChatBaseURL` helper to normalize base URLs for non-media chat requests.
- Added `applyXAIChatHeaders` to handle CLI chat-proxy-specific headers.
- Updated logic to differentiate between default, chat-proxy, and custom gateway base URLs.
- Extended test coverage to validate behavior of headers and base URL handling across scenarios.

Closes: #4182
This commit is contained in:
Luis Pater
2026-07-11 02:07:01 +08:00
parent 045a96428f
commit 3533484a1f
4 changed files with 222 additions and 16 deletions
+5 -1
View File
@@ -4,8 +4,12 @@ package xai
import "time"
const (
// DefaultAPIBaseURL is the default xAI Responses API base URL.
// DefaultAPIBaseURL is the default official xAI API base URL.
// Used for OAuth credential defaults, websocket, and media (image/video).
// Non-media HTTP chat is rewritten to CLIChatProxyBaseURL by the executor.
DefaultAPIBaseURL = "https://api.x.ai/v1"
// CLIChatProxyBaseURL is the Grok CLI chat-proxy base URL for non-image/video HTTP chat.
CLIChatProxyBaseURL = "https://cli-chat-proxy.grok.com/v1"
// Issuer is xAI's OAuth issuer.
Issuer = "https://auth.x.ai"
// DiscoveryURL is the OIDC discovery endpoint used to resolve OAuth endpoints.
+51 -15
View File
@@ -60,6 +60,11 @@ const (
xaiVideosPath = "/videos"
xaiIdempotencyKeyMetaKey = "idempotency_key"
xaiComposerModelPrefix = "grok-composer-"
xaiTokenAuthHeader = "X-XAI-Token-Auth"
xaiTokenAuthValue = "xai-grok-cli"
xaiClientVersionHeader = "x-grok-client-version"
// Keep in sync with the current Grok CLI client version that chat-proxy expects.
xaiClientVersionValue = "0.2.93"
)
// XAIExecutor is a stateless executor for xAI Grok's Responses API.
@@ -121,10 +126,8 @@ func (e *XAIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
return e.executeVideos(ctx, auth, req, opts)
}
token, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
token, _ := xaiCreds(auth)
baseURL := xaiChatBaseURL(auth)
prepared, err := e.prepareResponsesRequest(ctx, req, opts, true)
if err != nil {
@@ -140,7 +143,7 @@ func (e *XAIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
if err != nil {
return resp, err
}
applyXAIHeaders(httpReq, auth, token, true, prepared.sessionID)
applyXAIChatHeaders(httpReq, auth, token, true, prepared.sessionID)
e.recordXAIRequest(ctx, auth, url, httpReq.Header.Clone(), prepared.body)
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
@@ -212,10 +215,8 @@ func (e *XAIExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.Aut
}
func (e *XAIExecutor) executeCompactRequest(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (*xaiPreparedRequest, []byte, http.Header, error) {
token, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
token, _ := xaiCreds(auth)
baseURL := xaiChatBaseURL(auth)
prepared, err := e.prepareResponsesRequestTo(ctx, req, opts, false, sdktranslator.FormatOpenAIResponse)
if err != nil {
@@ -234,7 +235,7 @@ func (e *XAIExecutor) executeCompactRequest(ctx context.Context, auth *cliproxya
if err != nil {
return nil, nil, nil, err
}
applyXAIHeaders(httpReq, auth, token, false, prepared.sessionID)
applyXAIChatHeaders(httpReq, auth, token, false, prepared.sessionID)
e.recordXAIRequest(ctx, auth, requestURL, httpReq.Header.Clone(), prepared.body)
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
@@ -579,10 +580,8 @@ func (e *XAIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth
return e.executeCompactionTriggerStream(ctx, auth, req, opts)
}
token, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL
}
token, _ := xaiCreds(auth)
baseURL := xaiChatBaseURL(auth)
prepared, err := e.prepareResponsesRequest(ctx, req, opts, true)
if err != nil {
@@ -598,7 +597,7 @@ func (e *XAIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth
if err != nil {
return nil, err
}
applyXAIHeaders(httpReq, auth, token, true, prepared.sessionID)
applyXAIChatHeaders(httpReq, auth, token, true, prepared.sessionID)
e.recordXAIRequest(ctx, auth, url, httpReq.Header.Clone(), prepared.body)
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
@@ -915,6 +914,31 @@ func xaiCreds(auth *cliproxyauth.Auth) (token, baseURL string) {
return token, baseURL
}
// xaiChatBaseURL returns the base URL for non-image/video xAI HTTP chat requests.
// Empty or official default base_url is rewritten to the CLI chat-proxy endpoint.
// An explicit non-default base_url (tests / custom gateways) is still honored.
// Websocket transport intentionally does not use this helper: cli-chat-proxy only
// accepts HTTP POST and returns 405 for websocket upgrades.
func xaiChatBaseURL(auth *cliproxyauth.Auth) string {
_, baseURL := xaiCreds(auth)
if baseURL != "" && !xaiIsDefaultAPIBaseURL(baseURL) {
return baseURL
}
return xaiauth.CLIChatProxyBaseURL
}
func xaiNormalizeBaseURL(baseURL string) string {
return strings.TrimRight(strings.TrimSpace(baseURL), "/")
}
func xaiIsDefaultAPIBaseURL(baseURL string) bool {
return xaiNormalizeBaseURL(baseURL) == xaiNormalizeBaseURL(xaiauth.DefaultAPIBaseURL)
}
func xaiIsCLIChatProxyBaseURL(baseURL string) bool {
return xaiNormalizeBaseURL(baseURL) == xaiNormalizeBaseURL(xaiauth.CLIChatProxyBaseURL)
}
func applyXAIHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, sessionID string) {
r.Header.Set("Content-Type", "application/json")
if strings.TrimSpace(token) != "" {
@@ -936,6 +960,18 @@ func applyXAIHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, str
util.ApplyCustomHeadersFromAttrs(r, attrs)
}
// applyXAIChatHeaders applies standard xAI headers for non-image/video chat
// requests. CLI chat-proxy identity headers are only attached when the resolved
// chat base URL is the official CLI chat-proxy endpoint.
func applyXAIChatHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, sessionID string) {
applyXAIHeaders(r, auth, token, stream, sessionID)
if !xaiIsCLIChatProxyBaseURL(xaiChatBaseURL(auth)) {
return
}
r.Header.Set(xaiTokenAuthHeader, xaiTokenAuthValue)
r.Header.Set(xaiClientVersionHeader, xaiClientVersionValue)
}
func xaiResolveComposerSessionID(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, baseModel string) (string, error) {
if sessionID := xaiExecutionSessionID(req, opts); sessionID != "" {
return sessionID, nil
@@ -13,6 +13,7 @@ import (
"testing"
"github.com/google/uuid"
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/translator"
@@ -806,11 +807,15 @@ func TestXAIExecutorExecuteImagesUsesImagesEndpoint(t *testing.T) {
var gotPath string
var gotAuth string
var gotAccept string
var gotTokenAuth string
var gotClientVersion string
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
gotAccept = r.Header.Get("Accept")
gotTokenAuth = r.Header.Get(xaiTokenAuthHeader)
gotClientVersion = r.Header.Get(xaiClientVersionHeader)
var errRead error
gotBody, errRead = io.ReadAll(r.Body)
if errRead != nil {
@@ -853,6 +858,12 @@ func TestXAIExecutorExecuteImagesUsesImagesEndpoint(t *testing.T) {
if gotAccept != "application/json" {
t.Fatalf("Accept = %q, want application/json", gotAccept)
}
if gotTokenAuth != "" {
t.Fatalf("%s = %q, want empty on media path", xaiTokenAuthHeader, gotTokenAuth)
}
if gotClientVersion != "" {
t.Fatalf("%s = %q, want empty on media path", xaiClientVersionHeader, gotClientVersion)
}
if string(gotBody) != `{"model":"grok-imagine-image","prompt":"draw"}` {
t.Fatalf("body = %s", string(gotBody))
}
@@ -1562,6 +1573,158 @@ func TestXAIExecutorReasoningReplayCacheReplaysFunctionCallForClaudeToolResult(t
}
}
func TestXAIChatBaseURL(t *testing.T) {
tests := []struct {
name string
auth *cliproxyauth.Auth
want string
}{
{
name: "nil auth defaults to chat proxy",
auth: nil,
want: xaiauth.CLIChatProxyBaseURL,
},
{
name: "empty base url defaults to chat proxy",
auth: &cliproxyauth.Auth{Provider: "xai"},
want: xaiauth.CLIChatProxyBaseURL,
},
{
name: "official default rewrites to chat proxy",
auth: &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": xaiauth.DefaultAPIBaseURL},
},
want: xaiauth.CLIChatProxyBaseURL,
},
{
name: "official default with trailing slash rewrites to chat proxy",
auth: &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": xaiauth.DefaultAPIBaseURL + "/"},
},
want: xaiauth.CLIChatProxyBaseURL,
},
{
name: "metadata official default rewrites to chat proxy",
auth: &cliproxyauth.Auth{
Metadata: map[string]any{"base_url": xaiauth.DefaultAPIBaseURL},
},
want: xaiauth.CLIChatProxyBaseURL,
},
{
name: "custom base url is honored",
auth: &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": "https://gateway.example.com/v1"},
},
want: "https://gateway.example.com/v1",
},
{
name: "explicit chat proxy base url is preserved",
auth: &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": xaiauth.CLIChatProxyBaseURL},
},
want: xaiauth.CLIChatProxyBaseURL,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := xaiChatBaseURL(tt.auth); got != tt.want {
t.Fatalf("xaiChatBaseURL() = %q, want %q", got, tt.want)
}
})
}
}
func TestApplyXAIChatHeaders(t *testing.T) {
t.Run("cli chat proxy headers on default base", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://example.invalid/responses", nil)
auth := &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": xaiauth.DefaultAPIBaseURL},
}
applyXAIChatHeaders(req, auth, "xai-token", true, "conv-1")
if got := req.Header.Get("Authorization"); got != "Bearer xai-token" {
t.Fatalf("Authorization = %q, want Bearer xai-token", got)
}
if got := req.Header.Get("x-grok-conv-id"); got != "conv-1" {
t.Fatalf("x-grok-conv-id = %q, want conv-1", got)
}
if got := req.Header.Get(xaiTokenAuthHeader); got != xaiTokenAuthValue {
t.Fatalf("%s = %q, want %q", xaiTokenAuthHeader, got, xaiTokenAuthValue)
}
if got := req.Header.Get(xaiClientVersionHeader); got != xaiClientVersionValue {
t.Fatalf("%s = %q, want %q", xaiClientVersionHeader, got, xaiClientVersionValue)
}
})
t.Run("no cli headers on custom gateway", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://gateway.example.com/responses", nil)
auth := &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": "https://gateway.example.com/v1"},
}
applyXAIChatHeaders(req, auth, "xai-token", false, "")
if got := req.Header.Get(xaiTokenAuthHeader); got != "" {
t.Fatalf("%s = %q, want empty for custom gateway", xaiTokenAuthHeader, got)
}
if got := req.Header.Get(xaiClientVersionHeader); got != "" {
t.Fatalf("%s = %q, want empty for custom gateway", xaiClientVersionHeader, got)
}
})
t.Run("cli headers on explicit chat proxy base", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, xaiauth.CLIChatProxyBaseURL+"/responses", nil)
auth := &cliproxyauth.Auth{
Attributes: map[string]string{"base_url": xaiauth.CLIChatProxyBaseURL + "/"},
}
applyXAIChatHeaders(req, auth, "xai-token", true, "")
if got := req.Header.Get(xaiTokenAuthHeader); got != xaiTokenAuthValue {
t.Fatalf("%s = %q, want %q", xaiTokenAuthHeader, got, xaiTokenAuthValue)
}
if got := req.Header.Get(xaiClientVersionHeader); got != xaiClientVersionValue {
t.Fatalf("%s = %q, want %q", xaiClientVersionHeader, got, xaiClientVersionValue)
}
})
}
func TestXAIExecutorExecuteChatUsesProxyHeadersOnlyForChatProxy(t *testing.T) {
var gotTokenAuth string
var gotClientVersion string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotTokenAuth = r.Header.Get(xaiTokenAuthHeader)
gotClientVersion = r.Header.Get(xaiClientVersionHeader)
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\"}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n"))
}))
defer server.Close()
exec := NewXAIExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Provider: "xai",
Attributes: map[string]string{
"base_url": server.URL,
},
Metadata: map[string]any{"access_token": "xai-token"},
}
_, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "grok-4.3",
Payload: []byte(`{"model":"grok-4.3","input":[{"role":"user","content":"hello"}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatOpenAIResponse,
})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if gotTokenAuth != "" {
t.Fatalf("%s = %q, want empty for custom chat gateway", xaiTokenAuthHeader, gotTokenAuth)
}
if gotClientVersion != "" {
t.Fatalf("%s = %q, want empty for custom chat gateway", xaiClientVersionHeader, gotClientVersion)
}
}
func testValidGrokEncryptedContentForSeed(seed byte) string {
buf := make([]byte, 0, 256)
for i := 0; len(buf) < 256; i++ {
@@ -402,6 +402,9 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
return e.executeCompactionTriggerFromWebsocketContext(ctx, auth, req, opts, idMapper)
}
// Keep websocket on the official API base URL (or an explicit non-default
// base_url). Do not reuse xaiChatBaseURL: cli-chat-proxy only accepts HTTP
// POST and returns 405 for websocket upgrades.
token, baseURL := xaiCreds(auth)
if baseURL == "" {
baseURL = xaiauth.DefaultAPIBaseURL