Merge pull request #7780 from lyen1688/fix/grok-cli-identity-1-0-46

修复 Grok 426 并对齐交互式 CLI 身份头
This commit is contained in:
Wesley Liddick
2026-10-01 17:48:26 +08:00
committed by GitHub
11 changed files with 104 additions and 47 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, "interactive", req.Header.Get("x-grok-client-mode"))
}
func TestBuildBillingSummaryWeeklyAndMonthly(t *testing.T) {
+22 -7
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"
@@ -24,11 +25,11 @@ const (
// CLITokenAuth is required by cli-chat-proxy for Grok Build OAuth tokens.
CLITokenAuth = "xai-grok-cli"
// CLIClientIdentifier is the x-grok-client-identifier value used by Grok shell/CLI.
CLIClientIdentifier = "grok-shell"
// CLIClientIdentifier 对齐官方交互式 CLI 主请求的客户端标识。
CLIClientIdentifier = "grok-pager"
// CLIClientMode is used by billing / quota probes on the CLI surface.
CLIClientMode = "cli"
// CLIClientMode 对齐官方 CLI 正常交互模式。
CLIClientMode = "interactive"
)
// 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-pager/" + version + " 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 TestCLIUserAgentMatchesOfficialInteractiveCapture(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-pager/1.0.46 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, "interactive", 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-pager", capturedHeaders.Get("x-grok-client-identifier"))
require.Equal(t, "interactive", 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")
@@ -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,9 +36,9 @@ 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, "grok-shell", req.Header.Get("x-grok-client-identifier"))
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-pager", req.Header.Get("x-grok-client-identifier"))
}
func TestResolveGrokUpstreamUserAgentNeverPassthrough(t *testing.T) {
@@ -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) {
+1 -1
View File
@@ -21,7 +21,7 @@
"@stripe/stripe-js": "^9.0.1",
"@tanstack/vue-virtual": "^3.13.23",
"@vueuse/core": "^10.7.0",
"axios": "^1.18.0",
"axios": "^1.20.0",
"chart.js": "^4.4.1",
"dompurify": "^3.4.14",
"driver.js": "^1.4.0",
+5 -5
View File
@@ -30,8 +30,8 @@ importers:
specifier: ^10.7.0
version: 10.11.1(vue@3.5.26(typescript@5.6.3))
axios:
specifier: ^1.18.0
version: 1.18.1
specifier: ^1.20.0
version: 1.20.0
chart.js:
specifier: ^4.4.1
version: 4.5.1
@@ -1858,8 +1858,8 @@ packages:
peerDependencies:
postcss: '>=8.5.18'
axios@1.18.1:
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
axios@1.20.0:
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
@@ -6484,7 +6484,7 @@ snapshots:
postcss: 8.5.23
postcss-value-parser: 4.2.0
axios@1.18.1:
axios@1.20.0:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.6