对已合并 PR #5666 + 分组入口放行后的全量功能审计发现的 P0/P1 修复, 全部先经代码与厂商文档实证再实施: 1. /v1/messages 调度闸门(P0):sanitizeGroupMessagesDispatchFields 对非 openai 平台恒置 AllowMessagesDispatch=false,而闸门豁免名单只有 grok, CN 分组经正常途径创建后恒 403——原生 Anthropic 直通(Claude Code 主用例) 完全不可达。修复:闸门对 CN 与 grok 同语义豁免;count_tokens 处的内联 裸检查统一走同一 helper;ResolveMessagesDispatchModel 对 CN 早退,避免 openai 专属的 gpt-5.x 默认映射发给 CN 上游。 2. 计费候选链(P0):候选链兜底含客户端原始模型名,配合 getFallbackPricing 的 claude→Sonnet 统一兜底,映射的 CN 模型无价时 CN 流量会按 Claude 原价 (数倍~数十倍)静默误计,且 usage 日志显示 claude-* 名无从察觉。修复: CN 账号的 claude-* 候选仅在显式分组/渠道定价时放行;候选全滤空时按 ErrModelPricingUnavailable 走零成本+告警落账(顺带修复原空候选错误会 丢弃整条 usage 记录的次生问题)。 3. 断开/中断漏记(P0,#5148 对齐,惠及 openai 平台):messages/responses/ chat_completions 三个 handler 的错误路径此前在 err!=nil 时丢弃携带的 部分 result——客户端断开排水后的完整 usage 被丢,payg 上游照常计费而 平台漏记(anthropic 网关早有同修复,openai 网关缺失)。修复:错误路径 result 非空时照常提交 usage;failover 错误恒 result=nil 无重复计费。 Responses×anthropic 流式转换器同时改为断开后继续排水至流自然结束 (末尾 message_delta 的 output_tokens 不再丢),finalize 帧补工具名反转 与客户端工具还原、仅在客户端仍连接时写出。 4. count_tokens(P1,证据修正):经实证三家 Anthropic 兼容层均无 /v1/messages/count_tokens(DeepSeek 官方文档无此端点且注明 anthropic-version 被忽略;OpenModel 标注该端点 Anthropic only), anthropic 协议转发上游=常态 404,且错误处置缺模型上下文会把不计费的 探测放大成整账号停调。修复:CN 全协议一律本地 tiktoken 估算(与 Grok 同方案),删除上游转发死代码。 5. 403 处置(P1):CN 此前落入通用 handleAuthError,单次 HTML 403(CDN/ 代理拦截页)即永久禁用,且 403 在 failover 集里会逐账号重放连环禁用 整组。修复:CN 与 openai 同口径——HTML 豁免 + 3 次累计 + 临时冷却。 新增回归测试 8 项:闸门豁免(含 openai 仍受控断言)、CN 调度映射空返回、 候选过滤三态、空候选零成本落账、断开排水 usage 完整性、HTML-403 零处罚、 结构化 403 首次临时停调。handler/service 全包测试通过。
30 lines
1.4 KiB
Go
30 lines
1.4 KiB
Go
package handler
|
||
|
||
// CN 分组 /v1/messages 调度闸门回归(修复:正常途径创建的 CN 分组曾恒 403):
|
||
// sanitizeGroupMessagesDispatchFields 对非 openai 平台强制 AllowMessagesDispatch
|
||
// =false,故 CN 分组必须与 grok 一样在闸门处豁免,否则原生 Anthropic 直通
|
||
//(Claude Code 主用例)永远不可达。
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestAllowOpenAICompatibleMessagesDispatch_CNProvidersExempt(t *testing.T) {
|
||
require.True(t, allowOpenAICompatibleMessagesDispatch(nil), "无 key 保持放行")
|
||
|
||
for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformGrok} {
|
||
apiKey := &service.APIKey{Group: &service.Group{Platform: platform, AllowMessagesDispatch: false}}
|
||
require.True(t, allowOpenAICompatibleMessagesDispatch(apiKey),
|
||
"%s 分组必须豁免 allow_messages_dispatch 闸门", platform)
|
||
}
|
||
|
||
// 非回归:openai 分组仍受开关控制。
|
||
openaiOff := &service.APIKey{Group: &service.Group{Platform: service.PlatformOpenAI, AllowMessagesDispatch: false}}
|
||
require.False(t, allowOpenAICompatibleMessagesDispatch(openaiOff))
|
||
openaiOn := &service.APIKey{Group: &service.Group{Platform: service.PlatformOpenAI, AllowMessagesDispatch: true}}
|
||
require.True(t, allowOpenAICompatibleMessagesDispatch(openaiOn))
|
||
}
|