upstream_capability.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. // Package openai_compat 提供 OpenAI 协议族在不同上游间的能力差异判定工具。
  2. //
  3. // 背景:sub2api 的 OpenAI APIKey 账号通过 base_url 接入多种第三方 OpenAI 兼容上游
  4. // (DeepSeek、Kimi、GLM、Qwen 等)。这些上游普遍只支持 /v1/chat/completions,
  5. // 不存在 /v1/responses 端点。但网关历史代码无差别走 CC→Responses 转换并打到
  6. // /v1/responses,导致兼容上游 404。
  7. //
  8. // 本包提供基于"账号探测标记"的能力判定,配合
  9. // internal/service/openai_apikey_responses_probe.go 在创建/修改账号时一次性
  10. // 探测并落标。
  11. //
  12. // 设计取舍:
  13. // - 不维护静态 host 白名单——避免新增厂商时必须改代码(讨论沉淀于
  14. // pensieve/short-term/knowledge/upstream-capability-detection-design-tradeoffs)
  15. // - 标记缺失时默认 true(即"走 Responses"),保持与重构前老代码完全一致的存量
  16. // 账号行为("现状即证据"原则;详见
  17. // pensieve/short-term/maxims/preserve-existing-runtime-behavior-when-replacing-logic-in-stateful-systems)
  18. package openai_compat
  19. // AccountResponsesSupport 描述账号上游对 OpenAI Responses API 的有效支持状态。
  20. //
  21. // 仅用于 platform=openai + type=apikey 的账号;其他账号类型不应调用本包判定。
  22. type AccountResponsesSupport int
  23. const (
  24. // ResponsesSupportUnknown 表示账号尚未完成能力探测(extra 字段缺失)。
  25. // 上游路由层应按"现状即证据"原则默认走 Responses,保持与重构前一致。
  26. ResponsesSupportUnknown AccountResponsesSupport = iota
  27. // ResponsesSupportYes 探测确认上游支持 /v1/responses。
  28. ResponsesSupportYes
  29. // ResponsesSupportNo 探测确认上游不支持 /v1/responses,应走
  30. // /v1/chat/completions 直转路径。
  31. ResponsesSupportNo
  32. )
  33. // ResponsesSupportMode 描述账号级 Responses API 路由覆盖模式。
  34. type ResponsesSupportMode string
  35. const (
  36. // ResponsesSupportModeAuto 表示跟随自动探测结果。
  37. ResponsesSupportModeAuto ResponsesSupportMode = "auto"
  38. // ResponsesSupportModeForceResponses 强制使用 /v1/responses。
  39. ResponsesSupportModeForceResponses ResponsesSupportMode = "force_responses"
  40. // ResponsesSupportModeForceChatCompletions 强制使用 /v1/chat/completions。
  41. ResponsesSupportModeForceChatCompletions ResponsesSupportMode = "force_chat_completions"
  42. )
  43. // ExtraKeyResponsesMode 是 accounts.extra JSON 中存储手动覆盖模式的键名。
  44. // 值类型为 string:auto=跟随探测,force_responses=强制 Responses,
  45. // force_chat_completions=强制 Chat Completions。
  46. const ExtraKeyResponsesMode = "openai_responses_mode"
  47. // ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储自动探测结果的键名。
  48. // 值类型为 bool:true=支持、false=不支持、键缺失=未探测。
  49. const ExtraKeyResponsesSupported = "openai_responses_supported"
  50. // NormalizeResponsesSupportMode 归一化账号级 Responses API 路由覆盖模式。
  51. // 缺失或非法值按 auto 处理,以保持存量行为。
  52. func NormalizeResponsesSupportMode(mode string) ResponsesSupportMode {
  53. switch ResponsesSupportMode(mode) {
  54. case ResponsesSupportModeForceResponses:
  55. return ResponsesSupportModeForceResponses
  56. case ResponsesSupportModeForceChatCompletions:
  57. return ResponsesSupportModeForceChatCompletions
  58. default:
  59. return ResponsesSupportModeAuto
  60. }
  61. }
  62. // ResolveResponsesSupport 从账号的 extra map 中读取手动覆盖模式与探测标记。
  63. //
  64. // 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按
  65. // "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI)。
  66. func ResolveResponsesSupport(extra map[string]any) AccountResponsesSupport {
  67. if extra == nil {
  68. return ResponsesSupportUnknown
  69. }
  70. if mode, ok := extra[ExtraKeyResponsesMode].(string); ok {
  71. switch NormalizeResponsesSupportMode(mode) {
  72. case ResponsesSupportModeForceResponses:
  73. return ResponsesSupportYes
  74. case ResponsesSupportModeForceChatCompletions:
  75. return ResponsesSupportNo
  76. }
  77. }
  78. v, ok := extra[ExtraKeyResponsesSupported]
  79. if !ok {
  80. return ResponsesSupportUnknown
  81. }
  82. supported, ok := v.(bool)
  83. if !ok {
  84. return ResponsesSupportUnknown
  85. }
  86. if supported {
  87. return ResponsesSupportYes
  88. }
  89. return ResponsesSupportNo
  90. }
  91. // ShouldUseResponsesAPI 判断 OpenAI APIKey 账号的入站 /v1/chat/completions 请求
  92. // 是否应走"CC→Responses 转换 + 上游 /v1/responses"路径。
  93. //
  94. // 返回 true 的两种情况:
  95. // 1. 账号已探测确认支持 Responses
  96. // 2. 账号未探测(标记缺失)——按"现状即证据"原则保留旧行为
  97. //
  98. // 仅当账号已探测且确认不支持时返回 false,此时调用方应走 CC 直转路径
  99. // (详见 internal/service/openai_gateway_chat_completions_raw.go)。
  100. func ShouldUseResponsesAPI(extra map[string]any) bool {
  101. return ResolveResponsesSupport(extra) != ResponsesSupportNo
  102. }