mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-19 16:17:56 +00:00
Merge pull request #7780 from lyen1688/fix/grok-cli-identity-1-0-46
修复 Grok 426 并对齐交互式 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, "interactive", 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"
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+5
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user