修复 Grok CLI 版本门槛并对齐官方无界面请求头

This commit is contained in:
lyen1688
2026-10-01 12:04:39 +08:00
parent 42bc7f6cff
commit fdce5b8d01
11 changed files with 97 additions and 40 deletions
+3 -5
View File
@@ -19,10 +19,7 @@ const (
// repository and service layers build their own client identity from it, so
// one bump here covers OAuth traffic and billing probes together.
// Keep in sync with https://x.ai/cli/stable.
CLIClientVersion = "0.2.120"
// billingCLIUserAgent is the legacy pager/shell UA used by billing probes.
// Distinct from CLIUserAgent() in cli_identity.go (workspace-style UA).
billingCLIUserAgent = "grok-pager/" + CLIClientVersion + " grok-shell/" + CLIClientVersion + " (macos; aarch64)"
CLIClientVersion = "1.0.46"
BillingWeeklyPath = "/billing?format=credits"
BillingMonthlyPath = "/billing"
@@ -148,7 +145,8 @@ func ApplyCLIBillingHeaders(req *http.Request, accessToken string) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set(CLITokenAuthHeader, CLITokenAuthValue)
req.Header.Set(CLIClientVersionHeader, CLIClientVersion)
req.Header.Set("User-Agent", billingCLIUserAgent)
req.Header.Set("User-Agent", CLIUserAgent(CLIClientVersion))
req.Header.Set("x-grok-client-mode", CLIClientMode)
}
// ParseBillingPayload unmarshals a billing API response body.
+2 -1
View File
@@ -38,7 +38,8 @@ func TestApplyCLIBillingHeaders(t *testing.T) {
require.Equal(t, "Bearer token", req.Header.Get("Authorization"))
require.Equal(t, CLITokenAuthValue, req.Header.Get(CLITokenAuthHeader))
require.Equal(t, CLIClientVersion, req.Header.Get(CLIClientVersionHeader))
require.Equal(t, "grok-pager/"+CLIClientVersion+" grok-shell/"+CLIClientVersion+" (macos; aarch64)", req.UserAgent())
require.Equal(t, CLIUserAgent(CLIClientVersion), req.UserAgent())
require.Equal(t, "headless", req.Header.Get("x-grok-client-mode"))
}
func TestBuildBillingSummaryWeeklyAndMonthly(t *testing.T) {
+20 -5
View File
@@ -3,6 +3,7 @@ package xai
import (
"net/http"
"os"
"runtime"
"strings"
"golang.org/x/mod/semver"
@@ -16,7 +17,7 @@ const (
CLIProxyHost = "cli-chat-proxy.grok.com"
// CLIStableVersion is the known-good minimum client version accepted by cli-chat-proxy.
CLIStableVersion = "0.2.93"
CLIStableVersion = "1.0.13"
// CLIVersionEnv is the optional operator override for CLIStableVersion.
CLIVersionEnv = "XAI_GROK_CLI_VERSION"
@@ -27,8 +28,8 @@ const (
// CLIClientIdentifier is the x-grok-client-identifier value used by Grok shell/CLI.
CLIClientIdentifier = "grok-shell"
// CLIClientMode is used by billing / quota probes on the CLI surface.
CLIClientMode = "cli"
// CLIClientMode 对齐官方 CLI 无界面运行模式。
CLIClientMode = "headless"
)
// ResolveCLIVersion returns a supported CLI client version.
@@ -54,12 +55,24 @@ func IsSupportedCLIVersion(version string) bool {
semver.Compare(canonical, minimum) >= 0
}
// CLIUserAgent builds the workspace-style User-Agent for a CLI client version.
// CLIUserAgent 对齐官方 CLI 无界面模式的 UA,平台名称使用 Rust 的格式。
func CLIUserAgent(version string) string {
if strings.TrimSpace(version) == "" {
version = CLIClientVersion
}
return "xai-grok-workspace/" + version
platform, arch := runtime.GOOS, runtime.GOARCH
if platform == "darwin" {
platform = "macos"
}
switch arch {
case "amd64":
arch = "x86_64"
case "arm64":
arch = "aarch64"
case "386":
arch = "x86"
}
return "grok-shell/" + version + " (" + platform + "; " + arch + ")"
}
// ApplyCLIProxyHeaders stamps the fixed Grok CLI identity when the request
@@ -75,5 +88,7 @@ func ApplyCLIProxyHeaders(req *http.Request) {
req.Header.Set("X-XAI-Token-Auth", CLITokenAuth)
req.Header.Set("x-grok-client-version", version)
req.Header.Set("x-grok-client-identifier", CLIClientIdentifier)
req.Header.Set("x-grok-client-mode", CLIClientMode)
req.Header.Set("x-authenticateresponse", "authenticate-response")
req.Header.Set("User-Agent", CLIUserAgent(version))
}
+42 -9
View File
@@ -2,11 +2,40 @@ package xai
import (
"net/http"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
)
func TestApplyCLIProxyHeadersMeetsUpstreamMinimumVersion(t *testing.T) {
// 上游 426 明确要求至少 1.0.13;断言独立于生产版本常量,防止旧覆盖值绕过下限。
for _, override := range []string{"", "0.2.93", "0.2.120", "1.0.12", "1.0.13-beta.1", "1.0.13", "1.0.14-alpha.1"} {
t.Run("override="+override, func(t *testing.T) {
t.Setenv(CLIVersionEnv, override)
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
ApplyCLIProxyHeaders(req)
version := req.Header.Get("x-grok-client-version")
require.True(t, semver.IsValid("v"+version))
require.GreaterOrEqual(t, semver.Compare("v"+version, "v1.0.13"), 0,
"Grok Responses 会以 426 拒绝版本 %s,最低要求为 1.0.13", version)
require.Contains(t, req.Header.Get("User-Agent"), "grok-shell/"+version+" (")
})
}
}
func TestCLIUserAgentMatchesOfficialHeadlessCapture(t *testing.T) {
// 来源:官方 CLI 1.0.46 在 Linux x86_64 的本地 Responses 抓包。
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
t.Skip("该抓包样本来自 Linux x86_64")
}
require.Equal(t, "grok-shell/1.0.46 (linux; x86_64)", CLIUserAgent("1.0.46"))
}
func TestResolveCLIVersionDefaultsToPinnedClientVersion(t *testing.T) {
t.Setenv(CLIVersionEnv, "")
// Default advertise pin is CLIClientVersion; CLIStableVersion is only the floor.
@@ -16,18 +45,18 @@ func TestResolveCLIVersionDefaultsToPinnedClientVersion(t *testing.T) {
}
func TestResolveCLIVersionAcceptsValidOverride(t *testing.T) {
t.Setenv(CLIVersionEnv, "0.2.95-alpha.1")
require.Equal(t, "0.2.95-alpha.1", ResolveCLIVersion())
t.Setenv(CLIVersionEnv, "1.0.14-alpha.1")
require.Equal(t, "1.0.14-alpha.1", ResolveCLIVersion())
}
func TestResolveCLIVersionRejectsUnsafeOrTooOld(t *testing.T) {
for _, version := range []string{
"0.2.92",
"0.2.93-beta.1",
"0.2.95\r\nX-Injected: true",
"0.2.093",
"0.3",
"1",
"1.0.12",
"1.0.13-beta.1",
"1.0.14\r\nX-Injected: true",
"1.0.014",
"1.1",
"2",
} {
t.Run(version, func(t *testing.T) {
t.Setenv(CLIVersionEnv, version)
@@ -48,11 +77,13 @@ func TestApplyCLIProxyHeaders(t *testing.T) {
require.Equal(t, CLIClientVersion, req.Header.Get("x-grok-client-version"))
require.Equal(t, CLIClientIdentifier, req.Header.Get("x-grok-client-identifier"))
require.Equal(t, CLITokenAuth, req.Header.Get("X-XAI-Token-Auth"))
require.Equal(t, "headless", req.Header.Get("x-grok-client-mode"))
require.Equal(t, "authenticate-response", req.Header.Get("x-authenticateresponse"))
require.Equal(t, CLIUserAgent(CLIClientVersion), req.Header.Get("User-Agent"))
}
func TestApplyCLIProxyHeadersLeavesAPIHostUnchanged(t *testing.T) {
t.Setenv(CLIVersionEnv, "0.2.95")
t.Setenv(CLIVersionEnv, "1.0.14")
req, err := http.NewRequest(http.MethodPost, "https://api.x.ai/v1/responses", nil)
require.NoError(t, err)
@@ -63,5 +94,7 @@ func TestApplyCLIProxyHeadersLeavesAPIHostUnchanged(t *testing.T) {
require.Empty(t, req.Header.Get("x-grok-client-version"))
require.Empty(t, req.Header.Get("x-grok-client-identifier"))
require.Empty(t, req.Header.Get("X-XAI-Token-Auth"))
require.Empty(t, req.Header.Get("x-grok-client-mode"))
require.Empty(t, req.Header.Get("x-authenticateresponse"))
require.Equal(t, "direct-api-client/1.0", req.Header.Get("User-Agent"))
}
@@ -466,6 +466,8 @@ func newGrokOfficialAPIFallbackRequest(req *http.Request) (*http.Request, error)
for _, header := range []string{
"X-XAI-Token-Auth",
"X-Grok-Client-Version",
"X-Grok-Client-Mode",
"X-Authenticateresponse",
"X-Grok-Client-Surface",
"X-UserID",
"X-Email",
@@ -531,6 +533,8 @@ func applyGrokCLIProxyHeaders(req *http.Request) {
req.Header.Set("X-XAI-Token-Auth", xai.CLITokenAuth)
req.Header.Set("x-grok-client-version", version)
req.Header.Set("x-grok-client-identifier", xai.CLIClientIdentifier)
req.Header.Set("x-grok-client-mode", xai.CLIClientMode)
req.Header.Set("x-authenticateresponse", "authenticate-response")
req.Header.Set("User-Agent", xai.CLIUserAgent(version))
}
@@ -237,6 +237,10 @@ func TestHTTPUpstreamDoAppliesGrokCLIIdentityBeforeOAuthRoundTrip(t *testing.T)
require.NoError(t, resp.Body.Close())
require.Equal(t, xai.CLIClientVersion, capturedHeaders.Get("x-grok-client-version"))
require.Equal(t, "1.0.46", capturedHeaders.Get("x-grok-client-version"))
require.Equal(t, "grok-shell", capturedHeaders.Get("x-grok-client-identifier"))
require.Equal(t, "headless", capturedHeaders.Get("x-grok-client-mode"))
require.Equal(t, "authenticate-response", capturedHeaders.Get("x-authenticateresponse"))
require.Equal(t, "xai-grok-cli", capturedHeaders.Get("X-XAI-Token-Auth"))
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), capturedHeaders.Get("User-Agent"))
})
@@ -309,6 +313,8 @@ func TestHTTPUpstreamDoFallsBackToOfficialGrokAPIOnCLIAccessDenied(t *testing.T)
require.Equal(t, "Bearer oauth-token", fallbackHeaders.Get("Authorization"))
require.Empty(t, fallbackHeaders.Get("X-XAI-Token-Auth"))
require.Empty(t, fallbackHeaders.Get("x-grok-client-version"))
require.Empty(t, fallbackHeaders.Get("x-grok-client-mode"))
require.Empty(t, fallbackHeaders.Get("x-authenticateresponse"))
require.Empty(t, fallbackHeaders.Get("User-Agent"))
}
@@ -465,18 +471,18 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
})
t.Run("accepts a valid operator override", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.121-alpha.1")
t.Setenv("XAI_GROK_CLI_VERSION", "1.0.47-alpha.1")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/chat/completions", nil)
require.NoError(t, err)
applyGrokCLIProxyHeaders(req)
require.Equal(t, "0.2.121-alpha.1", req.Header.Get("x-grok-client-version"))
require.Equal(t, xai.CLIUserAgent("0.2.121-alpha.1"), req.Header.Get("User-Agent"))
require.Equal(t, "1.0.47-alpha.1", req.Header.Get("x-grok-client-version"))
require.Equal(t, xai.CLIUserAgent("1.0.47-alpha.1"), req.Header.Get("User-Agent"))
})
t.Run("rejects an unsafe override", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.121\r\nX-Injected: true")
t.Setenv("XAI_GROK_CLI_VERSION", "1.0.47\r\nX-Injected: true")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
@@ -487,7 +493,7 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
})
t.Run("rejects an override below the supported minimum", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.119")
t.Setenv("XAI_GROK_CLI_VERSION", "1.0.45")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
@@ -498,7 +504,7 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
})
t.Run("rejects a prerelease override at the minimum version", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.120-beta.1")
t.Setenv("XAI_GROK_CLI_VERSION", "1.0.46-beta.1")
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
require.NoError(t, err)
@@ -511,11 +517,11 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
// Every entry sits above the pinned minimum, so a rejection here can only be
// caused by the malformed semver and never by the version being too old.
for _, version := range []string{
"0.2.0121",
"0.2.121-alpha..1",
"0.3",
"1",
"0.2.121+build.1",
"1.0.047",
"1.0.47-alpha..1",
"1.1",
"2",
"1.0.47+build.1",
} {
t.Run("rejects invalid semver "+version, func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", version)
@@ -530,7 +536,7 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
}
t.Run("leaves direct xAI API requests unchanged", func(t *testing.T) {
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.95")
t.Setenv("XAI_GROK_CLI_VERSION", "1.0.47")
req, err := http.NewRequest(http.MethodPost, "https://api.x.ai/v1/responses", nil)
require.NoError(t, err)
req.Header.Set("User-Agent", "direct-api-client/1.0")
@@ -136,7 +136,7 @@ func TestSyncGrokObservedModelsUsesCLIIdentityAndAccountHeaders(t *testing.T) {
require.Equal(t, xai.CLIClientVersion, upstream.lastReq.Header.Get("x-grok-client-version"))
require.Equal(t, xai.CLIClientIdentifier, upstream.lastReq.Header.Get("x-grok-client-identifier"))
require.Equal(t, xai.CLIUserAgent(xai.CLIClientVersion), upstream.lastReq.Header.Get("User-Agent"))
require.Equal(t, "interactive", upstream.lastReq.Header.Get("X-Grok-Client-Mode"))
require.Equal(t, "headless", upstream.lastReq.Header.Get("X-Grok-Client-Mode"))
require.Equal(t, "user-902", upstream.lastReq.Header.Get("X-UserID"))
require.Equal(t, "user902@example.test", upstream.lastReq.Header.Get("X-Email"))
require.Contains(t, repo.updates[account.ID], grokObservedModelsExtraKey)
@@ -17,7 +17,7 @@ const (
grokClientModeHeader = xai.CLIClientMode
)
// defaultGrokUpstreamUserAgent is the pinned Grok CLI / workspace UA.
// defaultGrokUpstreamUserAgent 使用固定版本的官方 CLI 无界面模式 UA。
// Grok upstream must not forward Claude Code / Codex / browser client UAs.
func defaultGrokUpstreamUserAgent() string {
return xai.CLIUserAgent(xai.ResolveCLIVersion())
@@ -28,7 +28,7 @@ func TestApplyDefaultGrokUpstreamHeadersUsesCLIUserAgent(t *testing.T) {
}
func TestApplyDefaultGrokUpstreamHeadersHonorsCLIVersionOverride(t *testing.T) {
t.Setenv(xai.CLIVersionEnv, "0.2.95")
t.Setenv(xai.CLIVersionEnv, "1.0.14")
req, err := http.NewRequest(http.MethodGet, "https://api.x.ai/v1/responses", nil)
require.NoError(t, err)
@@ -36,8 +36,8 @@ func TestApplyDefaultGrokUpstreamHeadersHonorsCLIVersionOverride(t *testing.T) {
applyDefaultGrokUpstreamHeaders(req)
require.Equal(t, "0.2.95", req.Header.Get("x-grok-client-version"))
require.Equal(t, xai.CLIUserAgent("0.2.95"), req.Header.Get("User-Agent"))
require.Equal(t, "1.0.14", req.Header.Get("x-grok-client-version"))
require.Equal(t, xai.CLIUserAgent("1.0.14"), req.Header.Get("User-Agent"))
require.Equal(t, "grok-shell", req.Header.Get("x-grok-client-identifier"))
}
@@ -1613,8 +1613,8 @@ func applyGrokCLIHeaders(headers http.Header) {
headers.Set("X-Grok-Client-Version", version)
headers.Set("x-grok-client-version", version)
headers.Set("x-grok-client-identifier", xai.CLIClientIdentifier)
// Historical mode value expected by some unit tests / older CLI probes.
headers.Set("X-Grok-Client-Mode", "interactive")
// 对齐官方 CLI 无界面模式,网关请求与额度探测共用身份。
headers.Set("X-Grok-Client-Mode", xai.CLIClientMode)
}
func (s *OpenAIGatewayService) updateGrokUsageSnapshot(ctx context.Context, account *Account, snapshot *xai.QuotaSnapshot) {
@@ -334,7 +334,7 @@ func TestBuildUpstreamModelsRequestSupportsGrokOAuth(t *testing.T) {
require.Equal(t, "https://cli-chat-proxy.grok.com/v1/models", req.URL.String())
require.Equal(t, "Bearer oauth-access-token", req.Header.Get("Authorization"))
require.Equal(t, grokCLIVersion, req.Header.Get("X-Grok-Client-Version"))
require.Equal(t, "interactive", req.Header.Get("X-Grok-Client-Mode"))
require.Equal(t, "headless", req.Header.Get("X-Grok-Client-Mode"))
require.Equal(t, defaultGrokUpstreamUserAgent(), req.Header.Get("User-Agent"))
require.Equal(t, "grok-user-id", req.Header.Get("X-UserID"))
require.Equal(t, "grok-user@example.com", req.Header.Get("X-Email"))