mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-11 06:31:39 +00: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 或更高版本——它们断言的正是本次有意改变
的行为,各自的原始意图(哨兵拒绝、合法升级生效、健康缓存不重写)保持不变。
147 lines
5.8 KiB
Go
147 lines
5.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
|
)
|
|
|
|
// floorClaudeCLIUserAgentVersion 单元测试:版本下限抬升的各种形态。
|
|
func TestFloorClaudeCLIUserAgentVersion(t *testing.T) {
|
|
floorUA := "claude-cli/" + claude.CLICurrentVersion
|
|
cases := []struct {
|
|
name string
|
|
ua string
|
|
want string
|
|
wantChanged bool
|
|
}{
|
|
// 线上故障形态:历史版本低于 CLICurrentVersion,就地抬到下限。
|
|
{"old_version_upgraded", "claude-cli/2.1.220 (external, cli)", floorUA + " (external, cli)", true},
|
|
// 只替换版本号段,括号内的真实客户端形态原样保留。
|
|
{"old_version_with_desktop_3p_suffix",
|
|
"claude-cli/2.1.100 (external, claude-desktop-3p, agent-sdk/0.3.100)",
|
|
floorUA + " (external, claude-desktop-3p, agent-sdk/0.3.100)", true},
|
|
// 等于下限:不得改动。
|
|
{"equal_to_floor", floorUA + " (external, cli)", floorUA + " (external, cli)", false},
|
|
// 高于下限:只升不降,不得把客户端上报的更新版本压回去。
|
|
{"newer_than_floor_not_downgraded", "claude-cli/2.9.0 (external, cli)", "claude-cli/2.9.0 (external, cli)", false},
|
|
// 非 claude-cli 产品:一律不动。
|
|
{"other_product_untouched", "opencode/1.2.3 (external, cli)", "opencode/1.2.3 (external, cli)", false},
|
|
// 空串 / 畸形:一律不动。
|
|
{"empty", "", "", false},
|
|
{"no_version", "claude-cli (external, cli)", "claude-cli (external, cli)", false},
|
|
{"unparseable_version", "claude-cli/abc (external, cli)", "claude-cli/abc (external, cli)", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, changed := floorClaudeCLIUserAgentVersion(tc.ua)
|
|
require.Equal(t, tc.want, got)
|
|
require.Equal(t, tc.wantChanged, changed)
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetOrCreateFingerprint 集成行为,直接对应线上故障:缓存指纹停留在历史版本
|
|
// 2.1.220(生产 Redis 中账号 147 的实际值),客户端送来更旧的 2.1.75。
|
|
// 修复前:isNewerVersion 不触发、UA 形态合法不触发自愈,旧指纹被原样返回并
|
|
// 近乎永不过期——上游按指纹 UA 做客户端版本闸门(Fable 5.1 要求 >= 2.1.251),
|
|
// 仅升 CLICurrentVersion 对存量账号完全无效。
|
|
func TestGetOrCreateFingerprintFloorsStaleCachedUserAgent(t *testing.T) {
|
|
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
|
UserAgent: "claude-cli/2.1.220 (external, cli)",
|
|
ClientID: "cid-1",
|
|
StainlessPackageVersion: "0.91.1",
|
|
UpdatedAt: time.Now().Unix(),
|
|
}}
|
|
svc := NewIdentityService(cache)
|
|
|
|
fp, err := svc.GetOrCreateFingerprint(
|
|
context.Background(), 147,
|
|
headersWithUA("claude-cli/2.1.75 (external, cli)"),
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
|
|
require.Equal(t, 1, cache.setCalls, "下限抬升必须持久化写回缓存")
|
|
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", cache.lastSet.UserAgent)
|
|
// X-Stainless-* 维持既有 merge 语义:客户端未携带时保留缓存中的真实值,不被下限逻辑覆盖。
|
|
require.Equal(t, "0.91.1", fp.StainlessPackageVersion)
|
|
}
|
|
|
|
// 缓存版本高于下限:不得被降级,也不得触发多余写入。
|
|
func TestGetOrCreateFingerprintDoesNotTouchCacheAboveFloor(t *testing.T) {
|
|
ua := "claude-cli/2.9.0 (external, cli)"
|
|
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
|
UserAgent: ua,
|
|
ClientID: "cid-1",
|
|
StainlessPackageVersion: "0.91.1",
|
|
UpdatedAt: time.Now().Unix(),
|
|
}}
|
|
svc := NewIdentityService(cache)
|
|
|
|
fp, err := svc.GetOrCreateFingerprint(
|
|
context.Background(), 1,
|
|
headersWithUA("claude-cli/2.1.75 (external, cli)"),
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, ua, fp.UserAgent, "高于下限的缓存版本不得被降级")
|
|
require.Zero(t, cache.setCalls)
|
|
}
|
|
|
|
// 客户端送来高于下限的更新版本:常规升级照常生效,且不被下限压回。
|
|
func TestGetOrCreateFingerprintClientNewerThanFloorStillWins(t *testing.T) {
|
|
newUA := "claude-cli/2.9.0 (external, cli)"
|
|
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
|
UserAgent: "claude-cli/2.1.220 (external, cli)",
|
|
ClientID: "cid-1",
|
|
UpdatedAt: time.Now().Unix(),
|
|
}}
|
|
svc := NewIdentityService(cache)
|
|
|
|
fp, err := svc.GetOrCreateFingerprint(context.Background(), 1, headersWithUA(newUA))
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, newUA, fp.UserAgent, "客户端更新版本优先于下限,不得被压回")
|
|
require.Equal(t, 1, cache.setCalls)
|
|
}
|
|
|
|
// 客户端版本高于缓存但低于下限:merge 升级后仍被抬到下限(二者取更新者)。
|
|
func TestGetOrCreateFingerprintFloorWinsOverStaleClientUpgrade(t *testing.T) {
|
|
cache := &stubIdentityCache{fingerprint: &Fingerprint{
|
|
UserAgent: "claude-cli/2.1.22 (external, cli)",
|
|
ClientID: "cid-1",
|
|
UpdatedAt: time.Now().Unix(),
|
|
}}
|
|
svc := NewIdentityService(cache)
|
|
|
|
fp, err := svc.GetOrCreateFingerprint(
|
|
context.Background(), 1,
|
|
headersWithUA("claude-cli/2.1.223 (external, cli)"),
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, cli)", fp.UserAgent)
|
|
require.Equal(t, 1, cache.setCalls)
|
|
}
|
|
|
|
// 首次创建路径同样受下限约束:合法但过旧的 claude-cli UA 落库时必须抬到 CLICurrentVersion。
|
|
func TestCreateFingerprintFromHeadersFloorsOldClientUserAgent(t *testing.T) {
|
|
cache := &stubIdentityCache{}
|
|
svc := NewIdentityService(cache)
|
|
|
|
fp, err := svc.GetOrCreateFingerprint(
|
|
context.Background(), 1,
|
|
headersWithUA("claude-cli/2.1.75 (external, claude-desktop-3p)"),
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, "claude-cli/"+claude.CLICurrentVersion+" (external, claude-desktop-3p)", fp.UserAgent)
|
|
require.Equal(t, 1, cache.setCalls)
|
|
}
|