| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- 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 supports responses", id: 203, platform: PlatformKimi, protocol: APIProtocolAdaptive, wantSupport: true, wantMode: string(openai_compat.ResponsesSupportModeForceResponses)},
- {name: "kimi responses protocol supports responses", id: 205, platform: PlatformKimi, protocol: APIProtocolResponses, wantSupport: true, wantMode: string(openai_compat.ResponsesSupportModeForceResponses)},
- {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))
- }
|