Files
sub2api/backend/internal/service/openai_apikey_responses_probe_test.go
T
李建琦 6d655c9903
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
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

161 lines
6.3 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 service
import (
"context"
"io"
"net/http"
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat"
"github.com/stretchr/testify/require"
)
func TestProbeOpenAIAPIKeyResponsesSupportUsesCodexProbeHeaders(t *testing.T) {
updateCalls := make(chan map[string]any, 1)
account := Account{
ID: 96,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Concurrency: 1,
Credentials: map[string]any{
"api_key": "sk-test",
"base_url": "https://compat-upstream.example/v1",
},
}
repo := &snapshotUpdateAccountRepo{
stubOpenAIAccountRepo: stubOpenAIAccountRepo{accounts: []Account{account}},
updateExtraCalls: updateCalls,
}
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"output":[{"type":"function_call","name":"probe_ping"}]}`)),
}}
svc := &AccountTestService{
accountRepo: repo,
httpUpstream: upstream,
cfg: &config.Config{Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{Enabled: false}}},
}
svc.ProbeOpenAIAPIKeyResponsesSupport(context.Background(), account.ID)
require.NotNil(t, upstream.lastReq)
require.Equal(t, "https://compat-upstream.example/v1/responses", upstream.lastReq.URL.String())
requireOpenAICodexProbeHeaders(t, upstream.lastReq.Header)
updates := <-updateCalls
require.Equal(t, true, updates[openai_compat.ExtraKeyResponsesSupported])
}
func TestProbeOpenAIAPIKeyResponsesSupportCNProviders(t *testing.T) {
tests := []struct {
name string
id int64
platform string
protocol string
wantSupport bool
wantMode string
}{
{name: "deepseek adaptive supports responses", id: 201, platform: PlatformDeepseek, protocol: APIProtocolAdaptive, wantSupport: true, wantMode: string(openai_compat.ResponsesSupportModeForceResponses)},
{name: "deepseek chat clears forced responses", id: 202, platform: PlatformDeepseek, protocol: APIProtocolChatCompletions, wantSupport: false, wantMode: string(openai_compat.ResponsesSupportModeAuto)},
{name: "kimi adaptive falls back to chat", id: 203, platform: PlatformKimi, protocol: APIProtocolAdaptive, wantSupport: false, wantMode: string(openai_compat.ResponsesSupportModeAuto)},
{name: "zhipu adaptive falls back to chat", id: 204, platform: PlatformZhipu, protocol: APIProtocolAdaptive, wantSupport: false, wantMode: string(openai_compat.ResponsesSupportModeAuto)},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
updateCalls := make(chan map[string]any, 1)
account := Account{
ID: tc.id, Platform: tc.platform, Type: AccountTypeAPIKey,
Credentials: map[string]any{"api_key": "sk-test", "api_protocol": tc.protocol},
Extra: map[string]any{
openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeForceResponses),
},
}
repo := &snapshotUpdateAccountRepo{
stubOpenAIAccountRepo: stubOpenAIAccountRepo{accounts: []Account{account}},
updateExtraCalls: updateCalls,
}
svc := &AccountTestService{accountRepo: repo}
svc.ProbeOpenAIAPIKeyResponsesSupport(context.Background(), account.ID)
updates := <-updateCalls
require.Equal(t, tc.wantSupport, updates[openai_compat.ExtraKeyResponsesSupported])
require.Equal(t, tc.wantMode, updates[openai_compat.ExtraKeyResponsesMode])
})
}
}
func TestDecideResponsesProbeSupport(t *testing.T) {
fnCall := []byte(`{"output":[{"type":"reasoning"},{"type":"function_call","name":"probe_ping"}]}`)
reasoningOnly := []byte(`{"output":[{"type":"reasoning"}]}`)
cases := []struct {
name string
status int
body []byte
want bool
}{
// Endpoint clearly absent on third-party OpenAI-compatible upstreams.
{"404 endpoint absent", 404, fnCall, false},
{"405 method not allowed", 405, fnCall, false},
// 2xx: tool capability is judged by presence of a function_call output item.
{"200 with function_call", 200, fnCall, true},
// Volcengine Ark coding/v3 × kimi-k2.6: reasoning only, no function_call.
{"200 reasoning only", 200, reasoningOnly, false},
{"200 invalid json", 200, []byte("not-json"), false},
{"200 no output field", 200, []byte(`{"status":"completed"}`), false},
// Non-2xx (other than 404/405): endpoint exists, capability undecidable -> conservative true.
{"400 conservative true", 400, reasoningOnly, true},
{"401 conservative true", 401, nil, true},
{"500 conservative true", 500, nil, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, decideResponsesProbeSupport(tc.status, tc.body))
})
}
}
func TestResponsesProbeBodyHasFunctionCall(t *testing.T) {
require.True(t, responsesProbeBodyHasFunctionCall([]byte(`{"output":[{"type":"function_call"}]}`)))
require.True(t, responsesProbeBodyHasFunctionCall([]byte(`{"output":[{"type":"reasoning"},{"type":"function_call"}]}`)))
require.False(t, responsesProbeBodyHasFunctionCall([]byte(`{"output":[{"type":"reasoning"}]}`)))
require.False(t, responsesProbeBodyHasFunctionCall([]byte(`{"output":[]}`)))
require.False(t, responsesProbeBodyHasFunctionCall([]byte(`{}`)))
require.False(t, responsesProbeBodyHasFunctionCall([]byte(`garbage`)))
}
func TestSelectResponsesProbeModel(t *testing.T) {
// No model_mapping -> fall back to DefaultTestModel (OpenAI official APIKey).
require.Equal(t, openai.DefaultTestModel, selectResponsesProbeModel(&Account{}))
// model_mapping values are upstream models; pick first by sort for reproducibility.
acct := &Account{Credentials: map[string]any{
"model_mapping": map[string]any{
"client-b": "zeta-model",
"client-a": "alpha-model",
},
}}
require.Equal(t, "alpha-model", selectResponsesProbeModel(acct))
// Wildcard / blank upstream values are skipped.
acctWild := &Account{Credentials: map[string]any{
"model_mapping": map[string]any{
"a": "*",
"b": " ",
"c": "real-model",
},
}}
require.Equal(t, "real-model", selectResponsesProbeModel(acctWild))
// Only wildcard mappings -> DefaultTestModel.
acctAllWild := &Account{Credentials: map[string]any{
"model_mapping": map[string]any{"a": "gpt-*"},
}}
require.Equal(t, openai.DefaultTestModel, selectResponsesProbeModel(acctAllWild))
}