mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 05:30:44 +08:00
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
56 lines
1.9 KiB
Go
56 lines
1.9 KiB
Go
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},
|
||
}
|
||
|
||
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},
|
||
}
|
||
|
||
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)
|
||
}
|
||
})
|
||
}
|
||
}
|