Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,115 @@
// Package openai_compat 提供 OpenAI 协议族在不同上游间的能力差异判定工具。
//
// 背景:sub2api 的 OpenAI APIKey 账号通过 base_url 接入多种第三方 OpenAI 兼容上游
// DeepSeek、Kimi、GLM、Qwen 等)。这些上游普遍只支持 /v1/chat/completions
// 不存在 /v1/responses 端点。但网关历史代码无差别走 CC→Responses 转换并打到
// /v1/responses,导致兼容上游 404。
//
// 本包提供基于"账号探测标记"的能力判定,配合
// internal/service/openai_apikey_responses_probe.go 在创建/修改账号时一次性
// 探测并落标。
//
// 设计取舍:
// - 不维护静态 host 白名单——避免新增厂商时必须改代码(讨论沉淀于
// pensieve/short-term/knowledge/upstream-capability-detection-design-tradeoffs
// - 标记缺失时默认 true(即"走 Responses"),保持与重构前老代码完全一致的存量
// 账号行为("现状即证据"原则;详见
// pensieve/short-term/maxims/preserve-existing-runtime-behavior-when-replacing-logic-in-stateful-systems
package openai_compat
// AccountResponsesSupport 描述账号上游对 OpenAI Responses API 的有效支持状态。
//
// 仅用于 platform=openai + type=apikey 的账号;其他账号类型不应调用本包判定。
type AccountResponsesSupport int
const (
// ResponsesSupportUnknown 表示账号尚未完成能力探测(extra 字段缺失)。
// 上游路由层应按"现状即证据"原则默认走 Responses,保持与重构前一致。
ResponsesSupportUnknown AccountResponsesSupport = iota
// ResponsesSupportYes 探测确认上游支持 /v1/responses。
ResponsesSupportYes
// ResponsesSupportNo 探测确认上游不支持 /v1/responses,应走
// /v1/chat/completions 直转路径。
ResponsesSupportNo
)
// ResponsesSupportMode 描述账号级 Responses API 路由覆盖模式。
type ResponsesSupportMode string
const (
// ResponsesSupportModeAuto 表示跟随自动探测结果。
ResponsesSupportModeAuto ResponsesSupportMode = "auto"
// ResponsesSupportModeForceResponses 强制使用 /v1/responses。
ResponsesSupportModeForceResponses ResponsesSupportMode = "force_responses"
// ResponsesSupportModeForceChatCompletions 强制使用 /v1/chat/completions。
ResponsesSupportModeForceChatCompletions ResponsesSupportMode = "force_chat_completions"
)
// ExtraKeyResponsesMode 是 accounts.extra JSON 中存储手动覆盖模式的键名。
// 值类型为 stringauto=跟随探测,force_responses=强制 Responses
// force_chat_completions=强制 Chat Completions。
const ExtraKeyResponsesMode = "openai_responses_mode"
// ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储自动探测结果的键名。
// 值类型为 booltrue=支持、false=不支持、键缺失=未探测。
const ExtraKeyResponsesSupported = "openai_responses_supported"
// NormalizeResponsesSupportMode 归一化账号级 Responses API 路由覆盖模式。
// 缺失或非法值按 auto 处理,以保持存量行为。
func NormalizeResponsesSupportMode(mode string) ResponsesSupportMode {
switch ResponsesSupportMode(mode) {
case ResponsesSupportModeForceResponses:
return ResponsesSupportModeForceResponses
case ResponsesSupportModeForceChatCompletions:
return ResponsesSupportModeForceChatCompletions
default:
return ResponsesSupportModeAuto
}
}
// ResolveResponsesSupport 从账号的 extra map 中读取手动覆盖模式与探测标记。
//
// 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按
// "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI)。
func ResolveResponsesSupport(extra map[string]any) AccountResponsesSupport {
if extra == nil {
return ResponsesSupportUnknown
}
if mode, ok := extra[ExtraKeyResponsesMode].(string); ok {
switch NormalizeResponsesSupportMode(mode) {
case ResponsesSupportModeForceResponses:
return ResponsesSupportYes
case ResponsesSupportModeForceChatCompletions:
return ResponsesSupportNo
}
}
v, ok := extra[ExtraKeyResponsesSupported]
if !ok {
return ResponsesSupportUnknown
}
supported, ok := v.(bool)
if !ok {
return ResponsesSupportUnknown
}
if supported {
return ResponsesSupportYes
}
return ResponsesSupportNo
}
// ShouldUseResponsesAPI 判断 OpenAI APIKey 账号的入站 /v1/chat/completions 请求
// 是否应走"CC→Responses 转换 + 上游 /v1/responses"路径。
//
// 返回 true 的两种情况:
// 1. 账号已探测确认支持 Responses
// 2. 账号未探测(标记缺失)——按"现状即证据"原则保留旧行为
//
// 仅当账号已探测且确认不支持时返回 false,此时调用方应走 CC 直转路径
// (详见 internal/service/openai_gateway_chat_completions_raw.go)。
func ShouldUseResponsesAPI(extra map[string]any) bool {
return ResolveResponsesSupport(extra) != ResponsesSupportNo
}
@@ -0,0 +1,88 @@
package openai_compat
import "testing"
func TestResolveResponsesSupport(t *testing.T) {
tests := []struct {
name string
extra map[string]any
want AccountResponsesSupport
}{
{"nil extra", nil, ResponsesSupportUnknown},
{"empty extra", map[string]any{}, ResponsesSupportUnknown},
{"key missing", map[string]any{"other": "value"}, ResponsesSupportUnknown},
{"value true", map[string]any{ExtraKeyResponsesSupported: true}, ResponsesSupportYes},
{"value false", map[string]any{ExtraKeyResponsesSupported: false}, ResponsesSupportNo},
{"value wrong type string", map[string]any{ExtraKeyResponsesSupported: "true"}, ResponsesSupportUnknown},
{"value wrong type number", map[string]any{ExtraKeyResponsesSupported: 1}, ResponsesSupportUnknown},
{"value nil", map[string]any{ExtraKeyResponsesSupported: nil}, ResponsesSupportUnknown},
{"force responses", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses)}, ResponsesSupportYes},
{"force chat completions", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions)}, ResponsesSupportNo},
{"auto follows probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeAuto), ExtraKeyResponsesSupported: false}, ResponsesSupportNo},
{"invalid mode follows probe", map[string]any{ExtraKeyResponsesMode: "bogus", ExtraKeyResponsesSupported: true}, ResponsesSupportYes},
{"force responses overrides probe false", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses), ExtraKeyResponsesSupported: false}, ResponsesSupportYes},
{"force chat completions overrides probe true", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions), ExtraKeyResponsesSupported: true}, ResponsesSupportNo},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := ResolveResponsesSupport(tc.extra)
if got != tc.want {
t.Errorf("ResolveResponsesSupport(%v) = %v, want %v", tc.extra, got, tc.want)
}
})
}
}
func TestShouldUseResponsesAPI(t *testing.T) {
tests := []struct {
name string
extra map[string]any
want bool
}{
// 关键不变量:未探测必须返回 true(保留旧行为)
{"unknown defaults to true (preserve old behavior)", nil, true},
{"unknown empty defaults to true", map[string]any{}, true},
{"unknown wrong type defaults to true", map[string]any{ExtraKeyResponsesSupported: "yes"}, true},
// 已探测:标记决定
{"explicitly supported", map[string]any{ExtraKeyResponsesSupported: true}, true},
{"explicitly unsupported", map[string]any{ExtraKeyResponsesSupported: false}, false},
// 手动覆盖:覆盖自动探测结果
{"force responses overrides unsupported probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses), ExtraKeyResponsesSupported: false}, true},
{"force chat completions overrides supported probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions), ExtraKeyResponsesSupported: true}, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := ShouldUseResponsesAPI(tc.extra)
if got != tc.want {
t.Errorf("ShouldUseResponsesAPI(%v) = %v, want %v", tc.extra, got, tc.want)
}
})
}
}
func TestNormalizeResponsesSupportMode(t *testing.T) {
tests := []struct {
name string
mode string
want ResponsesSupportMode
}{
{"empty", "", ResponsesSupportModeAuto},
{"auto", "auto", ResponsesSupportModeAuto},
{"force responses", "force_responses", ResponsesSupportModeForceResponses},
{"force chat completions", "force_chat_completions", ResponsesSupportModeForceChatCompletions},
{"invalid", "enabled", ResponsesSupportModeAuto},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := NormalizeResponsesSupportMode(tc.mode)
if got != tc.want {
t.Errorf("NormalizeResponsesSupportMode(%q) = %q, want %q", tc.mode, got, tc.want)
}
})
}
}