Files
sub2api/backend/internal/pkg/openai_compat/upstream_capability.go
alfadb-bot 4e4cc80971 fix(openai-gateway): route APIKey accounts to /v1/chat/completions when upstream lacks Responses API
OpenAI APIKey accounts with base_url pointing to third-party OpenAI-compatible
upstreams (DeepSeek, Kimi, GLM, Qwen, etc.) were failing because the gateway
unconditionally converted Chat Completions requests to Responses format and
forwarded to {base_url}/v1/responses, which only exists on OpenAI's official
endpoint.

Detection-based routing:
- Probe upstream capability on account create/update via a minimal POST to
  /v1/responses; HTTP 404/405 means 'unsupported', any other response means
  'supported'.
- Persist result as accounts.extra.openai_responses_supported (bool).
- ForwardAsChatCompletions branches at function entry: APIKey accounts with
  explicit support=false go through new forwardAsRawChatCompletions which
  passthrough-forwards CC body to /v1/chat/completions without protocol
  conversion.

Default behavior for accounts without the marker preserves the legacy
'always Responses' path — existing OpenAI APIKey accounts that were working
before this change continue to work without modification (the 'reality is
evidence' principle: an account that has been running implies upstream
capability).

Probe is fired async after Create / Update / BatchCreate; failures only log,
never block the admin flow. BulkUpdate omitted (low signal of base_url
changes; can be added if needed).

Implementation:
- New pkg internal/pkg/openai_compat: marker key + ShouldUseResponsesAPI
- New service file openai_apikey_responses_probe.go: probe + persist
- New service file openai_gateway_chat_completions_raw.go: CC pass-through
- Account test endpoint short-circuits with explicit message for
  probed-unsupported accounts (full CC test path is a TODO)

Zero schema changes, zero migrations, zero frontend changes, zero wire
modifications — all wired through existing AccountTestService injection.

Closes: DeepSeek-OpenAI account (id=128) production failure
2026-04-30 19:25:45 +08:00

76 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
)
// ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储探测结果的键名。
// 值类型为 booltrue=支持、false=不支持、键缺失=未探测。
const ExtraKeyResponsesSupported = "openai_responses_supported"
// ResolveResponsesSupport 从账号的 extra map 中读取探测标记。
//
// 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按
// "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI
func ResolveResponsesSupport(extra map[string]any) AccountResponsesSupport {
if extra == nil {
return ResponsesSupportUnknown
}
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
}