mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
claude-fable-5-1 已登记进 DefaultModels,但 OAuth 账号实际调用仍然全部失败:
400 claude_code_version_too_old
"Claude Code 2.1.220 does not support this model;
version 2.1.251 or newer is required."
两个独立的原因,缺一不可:
1. CLICurrentVersion 停留在 2.1.220,低于上游对该模型的客户端版本闸门。
升到 2.1.258(当前官方 CLI 最新版,npm @anthropic-ai/claude-code)。
2. 只升常量对所有已有账号无效。上游请求实际发送的 User-Agent 来自账号级
持久指纹(IdentityService,Redis fingerprint:<accountID>),不是
DefaultHeaders;defaultFingerprint 只在首次创建指纹时使用。
GetOrCreateFingerprint 的缓存命中路径只有两条改写分支:UA 畸形时自愈,
或客户端送来更新版本时升级。指纹 7 天 TTL 但活跃账号每 24h 续期,实际
近乎永不过期,系统内也没有重置入口——客户端版本比缓存旧时(例如缓存
2.1.220、客户端 2.1.75),存量账号会永远对上游声称 2.1.220。
生产实测:升级 CLICurrentVersion 并部署后仍报同样的 400,读 Redis 确认
出问题账号的缓存值为 "claude-cli/2.1.220 (external, cli)"。
修复:新增 floorClaudeCLIUserAgentVersion,把 CLICurrentVersion 作为缓存
指纹里 claude-cli 版本的下限,缓存命中与首次创建两条路径共用。
- 只对 claude-cli 产品生效,其它客户端一律不动
- 只升不降:版本等于或高于下限(含客户端上报的更新版本)不做任何改动
- 只替换版本号段,UA 其余部分原样保留,例如
"(external, claude-desktop-3p, agent-sdk/0.3.100)" 不受影响
- 与既有的"客户端更新版本"升级分支相互独立、二者取更新者
- X-Stainless-* 维持既有 merge 语义,不被下限逻辑覆盖
cc_version 无需单独处理:billing attribution block 的版本已由
syncBillingHeaderVersion 跟随实际发送的 User-Agent 同步。
测试:identity_service_version_floor_test.go 覆盖 helper 的 8 种 UA 形态与
5 个 GetOrCreateFingerprint 场景,其中一个直接复现线上故障(缓存 2.1.220 +
客户端 2.1.75 → 写回 2.1.258,Stainless 字段保持不变)。
constants_cli_version_test.go 锁定常量与 DefaultHeaders 的 UA 同源,并断言
不低于 Fable 5.1 的闸门下限。回退实现后对应用例确认转红。
identity_service_user_agent_validation_test.go 中 3 个把版本钉死在旧字面量
的既有用例改为钉 CLICurrentVersion 或更高版本——它们断言的正是本次有意改变
的行为,各自的原始意图(哨兵拒绝、合法升级生效、健康缓存不重写)保持不变。
71 lines
2.2 KiB
Go
71 lines
2.2 KiB
Go
package claude
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// fable51MinCLIVersion 是上游对 claude-fable-5-1 的客户端版本闸门下限。
|
||
// 低于该版本时上游直接 400:
|
||
//
|
||
// claude_code_version_too_old
|
||
// "Claude Code <ver> does not support this model; version 2.1.251 or newer is required."
|
||
const fable51MinCLIVersion = "2.1.251"
|
||
|
||
// TestCLICurrentVersionMatchesDefaultUserAgent 锁定"常量与 UA 同源"这条约束。
|
||
// 两者不一致会被 Anthropic 判为第三方客户端,且只改一处是历史上的常见疏漏,
|
||
// 所以这里从 UA 里反解版本号来比对,而不是并列两个字面量。
|
||
func TestCLICurrentVersionMatchesDefaultUserAgent(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
ua := DefaultHeaders["User-Agent"]
|
||
const prefix = "claude-cli/"
|
||
require.True(t, strings.HasPrefix(ua, prefix), "unexpected User-Agent: %q", ua)
|
||
|
||
rest := strings.TrimPrefix(ua, prefix)
|
||
version := rest
|
||
if idx := strings.IndexByte(rest, ' '); idx >= 0 {
|
||
version = rest[:idx]
|
||
}
|
||
require.Equal(t, CLICurrentVersion, version,
|
||
"DefaultHeaders User-Agent version must match CLICurrentVersion")
|
||
}
|
||
|
||
// TestCLICurrentVersionSatisfiesFable51Gate 锁定伪装版本不低于 claude-fable-5-1
|
||
// 的闸门下限。DefaultModels 里登记了该模型却发着更旧的版本号,等于模型清单可见
|
||
// 但一调就 400。
|
||
func TestCLICurrentVersionSatisfiesFable51Gate(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
require.GreaterOrEqual(t, compareSemver(t, CLICurrentVersion, fable51MinCLIVersion), 0,
|
||
"CLICurrentVersion %s is below the claude-fable-5-1 gate %s",
|
||
CLICurrentVersion, fable51MinCLIVersion)
|
||
}
|
||
|
||
// compareSemver 比较两个三段 semver,返回 -1 / 0 / 1。
|
||
func compareSemver(t *testing.T, a, b string) int {
|
||
t.Helper()
|
||
|
||
as, bs := strings.Split(a, "."), strings.Split(b, ".")
|
||
require.Len(t, as, 3, "not a three-part semver: %q", a)
|
||
require.Len(t, bs, 3, "not a three-part semver: %q", b)
|
||
|
||
for i := range as {
|
||
ai, err := strconv.Atoi(as[i])
|
||
require.NoError(t, err, "non-numeric segment in %q", a)
|
||
bi, err := strconv.Atoi(bs[i])
|
||
require.NoError(t, err, "non-numeric segment in %q", b)
|
||
|
||
switch {
|
||
case ai > bi:
|
||
return 1
|
||
case ai < bi:
|
||
return -1
|
||
}
|
||
}
|
||
return 0
|
||
}
|