mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 00:18:55 +00:00
修复 Grok CLI 版本门槛并对齐官方无界面请求头
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user