mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-09 01:46:48 +08:00
feat(auth): unify provider key handling with OpenAI compatibility support
- Replaced direct `strings.ToLower` usage with `util.OpenAICompatibleProviderKey` for generating provider keys. - Updated auth and executor workflows to use namespaced keys for OpenAI-compatible providers. - Adjusted tests to validate namespaced key handling, including new test cases for provider registration and execution logic. - Added `OpenAICompatibleProviderKey` helper in `util` for consistent key transformations. Closes: #3600
This commit is contained in:
@@ -12,6 +12,20 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const openAICompatibleProviderPrefix = "openai-compatible-"
|
||||
|
||||
// OpenAICompatibleProviderKey returns the internal provider key for an OpenAI-compatible provider.
|
||||
func OpenAICompatibleProviderKey(name string) string {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
if name == "" || name == "openai-compatibility" || strings.HasPrefix(name, openAICompatibleProviderPrefix) {
|
||||
if name == "" {
|
||||
return "openai-compatibility"
|
||||
}
|
||||
return name
|
||||
}
|
||||
return openAICompatibleProviderPrefix + name
|
||||
}
|
||||
|
||||
// GetProviderName determines all AI service providers capable of serving a registered model.
|
||||
// It first queries the global model registry to retrieve the providers backing the supplied model name.
|
||||
// When the model has not been registered yet, it falls back to legacy string heuristics to infer
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
)
|
||||
@@ -226,6 +227,7 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor
|
||||
if providerName == "" {
|
||||
providerName = "openai-compatibility"
|
||||
}
|
||||
internalProviderKey := util.OpenAICompatibleProviderKey(providerName)
|
||||
base := strings.TrimSpace(compat.BaseURL)
|
||||
disableCooling := compat.DisableCooling
|
||||
|
||||
@@ -241,7 +243,7 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor
|
||||
"source": fmt.Sprintf("config:%s[%s]", providerName, token),
|
||||
"base_url": base,
|
||||
"compat_name": compat.Name,
|
||||
"provider_key": providerName,
|
||||
"provider_key": internalProviderKey,
|
||||
}
|
||||
metadata := map[string]any{}
|
||||
if disableCooling {
|
||||
@@ -259,7 +261,7 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor
|
||||
addConfigHeadersToAttrs(compat.Headers, attrs)
|
||||
a := &coreauth.Auth{
|
||||
ID: id,
|
||||
Provider: providerName,
|
||||
Provider: internalProviderKey,
|
||||
Label: compat.Name,
|
||||
Prefix: prefix,
|
||||
Status: coreauth.StatusActive,
|
||||
@@ -283,7 +285,7 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor
|
||||
"source": fmt.Sprintf("config:%s[%s]", providerName, token),
|
||||
"base_url": base,
|
||||
"compat_name": compat.Name,
|
||||
"provider_key": providerName,
|
||||
"provider_key": internalProviderKey,
|
||||
}
|
||||
metadata := map[string]any{}
|
||||
if disableCooling {
|
||||
@@ -298,7 +300,7 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor
|
||||
addConfigHeadersToAttrs(compat.Headers, attrs)
|
||||
a := &coreauth.Auth{
|
||||
ID: id,
|
||||
Provider: providerName,
|
||||
Provider: internalProviderKey,
|
||||
Label: compat.Name,
|
||||
Prefix: prefix,
|
||||
Status: coreauth.StatusActive,
|
||||
|
||||
@@ -400,6 +400,43 @@ func TestConfigSynthesizer_OpenAICompat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSynthesizer_OpenAICompat_UsesNamespacedProviderKey(t *testing.T) {
|
||||
synth := NewConfigSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{
|
||||
OpenAICompatibility: []config.OpenAICompatibility{
|
||||
{
|
||||
Name: "kimi",
|
||||
BaseURL: "https://kimi-compatible.example.com/v1",
|
||||
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
||||
{APIKey: "test-key"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
auth := auths[0]
|
||||
if auth.Provider != "openai-compatible-kimi" {
|
||||
t.Fatalf("provider = %q, want openai-compatible-kimi", auth.Provider)
|
||||
}
|
||||
if auth.Attributes["provider_key"] != "openai-compatible-kimi" {
|
||||
t.Fatalf("provider_key = %q, want openai-compatible-kimi", auth.Attributes["provider_key"])
|
||||
}
|
||||
if auth.Attributes["compat_name"] != "kimi" {
|
||||
t.Fatalf("compat_name = %q, want kimi", auth.Attributes["compat_name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSynthesizer_VertexCompat(t *testing.T) {
|
||||
synth := NewConfigSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
@@ -639,7 +676,7 @@ func TestConfigSynthesizer_AllProviders(t *testing.T) {
|
||||
providers[a.Provider] = true
|
||||
}
|
||||
|
||||
expected := []string{"gemini", "claude", "codex", "compat", "vertex"}
|
||||
expected := []string{"gemini", "claude", "codex", "openai-compatible-compat", "vertex"}
|
||||
for _, p := range expected {
|
||||
if !providers[p] {
|
||||
t.Errorf("expected provider %s not found", p)
|
||||
|
||||
@@ -913,13 +913,13 @@ func openAICompatProviderKey(auth *Auth) string {
|
||||
}
|
||||
if auth.Attributes != nil {
|
||||
if providerKey := strings.TrimSpace(auth.Attributes["provider_key"]); providerKey != "" {
|
||||
return strings.ToLower(providerKey)
|
||||
return util.OpenAICompatibleProviderKey(providerKey)
|
||||
}
|
||||
if compatName := strings.TrimSpace(auth.Attributes["compat_name"]); compatName != "" {
|
||||
return strings.ToLower(compatName)
|
||||
return util.OpenAICompatibleProviderKey(compatName)
|
||||
}
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
return util.OpenAICompatibleProviderKey(auth.Provider)
|
||||
}
|
||||
|
||||
func openAICompatModelPoolKey(auth *Auth, requestedModel string) string {
|
||||
@@ -3063,7 +3063,7 @@ func (m *Manager) closestCooldownWait(providers []string, model string, attempt
|
||||
if auth == nil {
|
||||
continue
|
||||
}
|
||||
providerKey := strings.TrimSpace(strings.ToLower(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(auth)
|
||||
if _, ok := providerSet[providerKey]; !ok {
|
||||
continue
|
||||
}
|
||||
@@ -3123,7 +3123,7 @@ func (m *Manager) retryAllowed(attempt int, providers []string) bool {
|
||||
if auth == nil {
|
||||
continue
|
||||
}
|
||||
providerKey := strings.TrimSpace(strings.ToLower(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(auth)
|
||||
if _, ok := providerSet[providerKey]; !ok {
|
||||
continue
|
||||
}
|
||||
@@ -4017,7 +4017,7 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op
|
||||
}
|
||||
registryRef := registry.GetGlobalRegistry()
|
||||
for _, candidate := range m.auths {
|
||||
if candidate.Provider != provider || candidate.Disabled {
|
||||
if candidate == nil || executorKeyFromAuth(candidate) != provider || candidate.Disabled {
|
||||
continue
|
||||
}
|
||||
if pinnedAuthID != "" && candidate.ID != pinnedAuthID {
|
||||
@@ -4083,7 +4083,7 @@ func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cli
|
||||
if strings.TrimSpace(model) != "" {
|
||||
m.mu.RLock()
|
||||
for _, candidate := range m.auths {
|
||||
if candidate == nil || candidate.Provider != provider || candidate.Disabled {
|
||||
if candidate == nil || executorKeyFromAuth(candidate) != provider || candidate.Disabled {
|
||||
continue
|
||||
}
|
||||
if _, used := tried[candidate.ID]; used {
|
||||
@@ -4176,7 +4176,7 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m
|
||||
if disallowFreeAuth && isFreeCodexAuth(candidate) {
|
||||
continue
|
||||
}
|
||||
providerKey := strings.TrimSpace(strings.ToLower(candidate.Provider))
|
||||
providerKey := executorKeyFromAuth(candidate)
|
||||
if providerKey == "" {
|
||||
continue
|
||||
}
|
||||
@@ -4219,7 +4219,7 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m
|
||||
if selected == nil {
|
||||
return nil, nil, "", &Error{Code: "auth_not_found", Message: "selector returned no auth"}
|
||||
}
|
||||
providerKey := strings.TrimSpace(strings.ToLower(selected.Provider))
|
||||
providerKey := executorKeyFromAuth(selected)
|
||||
executor, okExecutor := m.Executor(providerKey)
|
||||
if !okExecutor {
|
||||
return nil, nil, "", &Error{Code: "executor_not_found", Message: "executor not registered"}
|
||||
@@ -4274,7 +4274,7 @@ func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model s
|
||||
if candidate == nil || candidate.Disabled {
|
||||
continue
|
||||
}
|
||||
if _, ok := providerSet[strings.TrimSpace(strings.ToLower(candidate.Provider))]; !ok {
|
||||
if _, ok := providerSet[executorKeyFromAuth(candidate)]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, used := tried[candidate.ID]; used {
|
||||
@@ -4554,7 +4554,7 @@ func (m *Manager) homeRuntimeAuthByID(sessionID string, authID string) (*Auth, P
|
||||
if auth == nil || !authWebsocketsEnabled(auth) {
|
||||
return nil, nil, "", false
|
||||
}
|
||||
providerKey := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(auth)
|
||||
if providerKey == "" {
|
||||
return nil, nil, "", false
|
||||
}
|
||||
@@ -4652,7 +4652,7 @@ func (m *Manager) pickNextViaHome(ctx context.Context, model string, opts clipro
|
||||
if homeAuthAlreadyTried(tried, auth.ID) {
|
||||
return nil, nil, "", repeatedHomeAuthError()
|
||||
}
|
||||
providerKey := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(&auth)
|
||||
if providerKey == "" {
|
||||
return nil, nil, "", &Error{Code: "invalid_auth", Message: "home returned auth without provider", HTTPStatus: http.StatusBadGateway}
|
||||
}
|
||||
@@ -4725,7 +4725,7 @@ func (m *Manager) findAllAntigravityCreditsCandidateAuths(ctx context.Context, r
|
||||
if !strings.Contains(strings.ToLower(strings.TrimSpace(routeModel)), "claude") {
|
||||
continue
|
||||
}
|
||||
providerKey := strings.TrimSpace(strings.ToLower(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(auth)
|
||||
executor, ok := m.executors[providerKey]
|
||||
if !ok {
|
||||
continue
|
||||
@@ -5338,9 +5338,16 @@ func executorKeyFromAuth(auth *Auth) string {
|
||||
if providerKey == "" {
|
||||
providerKey = compatName
|
||||
}
|
||||
return strings.ToLower(providerKey)
|
||||
return util.OpenAICompatibleProviderKey(providerKey)
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") {
|
||||
providerKey := strings.TrimSpace(auth.Label)
|
||||
if providerKey == "" {
|
||||
providerKey = "openai-compatibility"
|
||||
}
|
||||
return util.OpenAICompatibleProviderKey(providerKey)
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
const openAICompatPoolProviderKey = "openai-compatible-pool"
|
||||
|
||||
type openAICompatPoolExecutor struct {
|
||||
id string
|
||||
|
||||
@@ -169,18 +171,18 @@ func newOpenAICompatPoolTestManager(t *testing.T, alias string, models []interna
|
||||
m := NewManager(nil, nil, nil)
|
||||
m.SetConfig(cfg)
|
||||
if executor == nil {
|
||||
executor = &openAICompatPoolExecutor{id: "pool"}
|
||||
executor = &openAICompatPoolExecutor{id: openAICompatPoolProviderKey}
|
||||
}
|
||||
m.RegisterExecutor(executor)
|
||||
|
||||
auth := &Auth{
|
||||
ID: "pool-auth-" + t.Name(),
|
||||
Provider: "pool",
|
||||
Provider: openAICompatPoolProviderKey,
|
||||
Status: StatusActive,
|
||||
Attributes: map[string]string{
|
||||
"api_key": "test-key",
|
||||
"compat_name": "pool",
|
||||
"provider_key": "pool",
|
||||
"provider_key": openAICompatPoolProviderKey,
|
||||
},
|
||||
}
|
||||
if _, err := m.Register(context.Background(), auth); err != nil {
|
||||
@@ -188,7 +190,7 @@ func newOpenAICompatPoolTestManager(t *testing.T, alias string, models []interna
|
||||
}
|
||||
|
||||
reg := registry.GetGlobalRegistry()
|
||||
reg.RegisterClient(auth.ID, "pool", []*registry.ModelInfo{{ID: alias}})
|
||||
reg.RegisterClient(auth.ID, openAICompatPoolProviderKey, []*registry.ModelInfo{{ID: alias}})
|
||||
t.Cleanup(func() {
|
||||
reg.UnregisterClient(auth.ID)
|
||||
})
|
||||
@@ -214,7 +216,7 @@ func TestManagerExecuteCount_OpenAICompatAliasPoolStopsOnInvalidRequest(t *testi
|
||||
alias := "claude-opus-4.66"
|
||||
invalidErr := &Error{HTTPStatus: http.StatusUnprocessableEntity, Message: "unprocessable entity"}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
countErrors: map[string]error{"deepseek-v3.1": invalidErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -222,7 +224,7 @@ func TestManagerExecuteCount_OpenAICompatAliasPoolStopsOnInvalidRequest(t *testi
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
_, err := m.ExecuteCount(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
_, err := m.ExecuteCount(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err == nil || err.Error() != invalidErr.Error() {
|
||||
t.Fatalf("execute count error = %v, want %v", err, invalidErr)
|
||||
}
|
||||
@@ -251,14 +253,14 @@ func TestResolveModelAliasPoolFromConfigModels(t *testing.T) {
|
||||
|
||||
func TestManagerExecute_OpenAICompatAliasPoolRotatesWithinAuth(t *testing.T) {
|
||||
alias := "claude-opus-4.66"
|
||||
executor := &openAICompatPoolExecutor{id: "pool"}
|
||||
executor := &openAICompatPoolExecutor{id: openAICompatPoolProviderKey}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
{Name: "deepseek-v3.1", Alias: alias},
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
resp, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %d: %v", i, err)
|
||||
}
|
||||
@@ -283,7 +285,7 @@ func TestManagerExecute_OpenAICompatAliasPoolStopsOnBadRequest(t *testing.T) {
|
||||
alias := "claude-opus-4.66"
|
||||
invalidErr := &Error{HTTPStatus: http.StatusBadRequest, Message: "invalid_request_error: malformed payload"}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
executeErrors: map[string]error{"deepseek-v3.1": invalidErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -291,7 +293,7 @@ func TestManagerExecute_OpenAICompatAliasPoolStopsOnBadRequest(t *testing.T) {
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
_, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
_, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err == nil || err.Error() != invalidErr.Error() {
|
||||
t.Fatalf("execute error = %v, want %v", err, invalidErr)
|
||||
}
|
||||
@@ -308,7 +310,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackOnModelSupportBadRequest(t
|
||||
Message: "invalid_request_error: The requested model is not supported.",
|
||||
}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
executeErrors: map[string]error{"deepseek-v3.1": modelSupportErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -316,7 +318,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackOnModelSupportBadRequest(t
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
resp, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute error = %v, want fallback success", err)
|
||||
}
|
||||
@@ -354,7 +356,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackOnModelSupportUnprocessabl
|
||||
Message: "The requested model is not supported.",
|
||||
}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
executeErrors: map[string]error{"deepseek-v3.1": modelSupportErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -362,7 +364,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackOnModelSupportUnprocessabl
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
resp, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute error = %v, want fallback success", err)
|
||||
}
|
||||
@@ -384,7 +386,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackOnModelSupportUnprocessabl
|
||||
func TestManagerExecute_OpenAICompatAliasPoolFallsBackWithinSameAuth(t *testing.T) {
|
||||
alias := "claude-opus-4.66"
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
executeErrors: map[string]error{"deepseek-v3.1": &Error{HTTPStatus: http.StatusTooManyRequests, Message: "quota"}},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -392,7 +394,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackWithinSameAuth(t *testing.
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
resp, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
@@ -411,7 +413,7 @@ func TestManagerExecute_OpenAICompatAliasPoolFallsBackWithinSameAuth(t *testing.
|
||||
func TestManagerExecuteStream_OpenAICompatAliasPoolRetriesOnEmptyBootstrap(t *testing.T) {
|
||||
alias := "claude-opus-4.66"
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
streamPayloads: map[string][]cliproxyexecutor.StreamChunk{
|
||||
"deepseek-v3.1": {},
|
||||
},
|
||||
@@ -421,7 +423,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolRetriesOnEmptyBootstrap(t *te
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute stream: %v", err)
|
||||
}
|
||||
@@ -447,7 +449,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolRetriesOnEmptyBootstrap(t *te
|
||||
func TestManagerExecuteStream_OpenAICompatAliasPoolFallsBackBeforeFirstByte(t *testing.T) {
|
||||
alias := "claude-opus-4.66"
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
streamFirstErrors: map[string]error{"deepseek-v3.1": &Error{HTTPStatus: http.StatusTooManyRequests, Message: "quota"}},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -455,7 +457,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolFallsBackBeforeFirstByte(t *t
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute stream: %v", err)
|
||||
}
|
||||
@@ -485,7 +487,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolStopsOnInvalidRequest(t *test
|
||||
alias := "claude-opus-4.66"
|
||||
invalidErr := &Error{HTTPStatus: http.StatusUnprocessableEntity, Message: "unprocessable entity"}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
streamFirstErrors: map[string]error{"deepseek-v3.1": invalidErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -493,7 +495,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolStopsOnInvalidRequest(t *test
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
_, err := m.ExecuteStream(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
_, err := m.ExecuteStream(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err == nil || err.Error() != invalidErr.Error() {
|
||||
t.Fatalf("execute stream error = %v, want %v", err, invalidErr)
|
||||
}
|
||||
@@ -510,7 +512,7 @@ func TestManagerExecute_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLaterReques
|
||||
Message: "invalid_request_error: The requested model is not supported.",
|
||||
}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
executeErrors: map[string]error{"deepseek-v3.1": modelSupportErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -519,7 +521,7 @@ func TestManagerExecute_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLaterReques
|
||||
}, executor)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
resp, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %d: %v", i, err)
|
||||
}
|
||||
@@ -547,7 +549,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLater
|
||||
Message: "The requested model is not supported.",
|
||||
}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
streamFirstErrors: map[string]error{"deepseek-v3.1": modelSupportErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -556,7 +558,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLater
|
||||
}, executor)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute stream %d: %v", i, err)
|
||||
}
|
||||
@@ -582,14 +584,14 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLater
|
||||
|
||||
func TestManagerExecuteCount_OpenAICompatAliasPoolRotatesWithinAuth(t *testing.T) {
|
||||
alias := "claude-opus-4.66"
|
||||
executor := &openAICompatPoolExecutor{id: "pool"}
|
||||
executor := &openAICompatPoolExecutor{id: openAICompatPoolProviderKey}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
{Name: "deepseek-v3.1", Alias: alias},
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
resp, err := m.ExecuteCount(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.ExecuteCount(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute count %d: %v", i, err)
|
||||
}
|
||||
@@ -614,7 +616,7 @@ func TestManagerExecuteCount_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLaterR
|
||||
Message: "invalid_request_error: The requested model is unsupported.",
|
||||
}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
countErrors: map[string]error{"deepseek-v3.1": modelSupportErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -623,7 +625,7 @@ func TestManagerExecuteCount_OpenAICompatAliasPoolSkipsSuspendedUpstreamOnLaterR
|
||||
}, executor)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
resp, err := m.ExecuteCount(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.ExecuteCount(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute count %d: %v", i, err)
|
||||
}
|
||||
@@ -659,27 +661,27 @@ func TestManagerExecute_OpenAICompatAliasPoolBlockedAuthDoesNotConsumeRetryBudge
|
||||
m.SetConfig(cfg)
|
||||
m.SetRetryConfig(0, 0, 1)
|
||||
|
||||
executor := &authScopedOpenAICompatPoolExecutor{id: "pool"}
|
||||
executor := &authScopedOpenAICompatPoolExecutor{id: openAICompatPoolProviderKey}
|
||||
m.RegisterExecutor(executor)
|
||||
|
||||
badAuth := &Auth{
|
||||
ID: "aa-blocked-auth",
|
||||
Provider: "pool",
|
||||
Provider: openAICompatPoolProviderKey,
|
||||
Status: StatusActive,
|
||||
Attributes: map[string]string{
|
||||
"api_key": "bad-key",
|
||||
"compat_name": "pool",
|
||||
"provider_key": "pool",
|
||||
"provider_key": openAICompatPoolProviderKey,
|
||||
},
|
||||
}
|
||||
goodAuth := &Auth{
|
||||
ID: "bb-good-auth",
|
||||
Provider: "pool",
|
||||
Provider: openAICompatPoolProviderKey,
|
||||
Status: StatusActive,
|
||||
Attributes: map[string]string{
|
||||
"api_key": "good-key",
|
||||
"compat_name": "pool",
|
||||
"provider_key": "pool",
|
||||
"provider_key": openAICompatPoolProviderKey,
|
||||
},
|
||||
}
|
||||
if _, err := m.Register(context.Background(), badAuth); err != nil {
|
||||
@@ -690,8 +692,8 @@ func TestManagerExecute_OpenAICompatAliasPoolBlockedAuthDoesNotConsumeRetryBudge
|
||||
}
|
||||
|
||||
reg := registry.GetGlobalRegistry()
|
||||
reg.RegisterClient(badAuth.ID, "pool", []*registry.ModelInfo{{ID: alias}})
|
||||
reg.RegisterClient(goodAuth.ID, "pool", []*registry.ModelInfo{{ID: alias}})
|
||||
reg.RegisterClient(badAuth.ID, openAICompatPoolProviderKey, []*registry.ModelInfo{{ID: alias}})
|
||||
reg.RegisterClient(goodAuth.ID, openAICompatPoolProviderKey, []*registry.ModelInfo{{ID: alias}})
|
||||
t.Cleanup(func() {
|
||||
reg.UnregisterClient(badAuth.ID)
|
||||
reg.UnregisterClient(goodAuth.ID)
|
||||
@@ -704,14 +706,14 @@ func TestManagerExecute_OpenAICompatAliasPoolBlockedAuthDoesNotConsumeRetryBudge
|
||||
for _, upstreamModel := range []string{"deepseek-v3.1", "glm-5"} {
|
||||
m.MarkResult(context.Background(), Result{
|
||||
AuthID: badAuth.ID,
|
||||
Provider: "pool",
|
||||
Provider: openAICompatPoolProviderKey,
|
||||
Model: upstreamModel,
|
||||
Success: false,
|
||||
Error: modelSupportErr,
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := m.Execute(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
resp, err := m.Execute(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("execute error = %v, want success via fallback auth", err)
|
||||
}
|
||||
@@ -732,7 +734,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolStopsOnInvalidBootstrap(t *te
|
||||
alias := "claude-opus-4.66"
|
||||
invalidErr := &Error{HTTPStatus: http.StatusBadRequest, Message: "invalid_request_error: malformed payload"}
|
||||
executor := &openAICompatPoolExecutor{
|
||||
id: "pool",
|
||||
id: openAICompatPoolProviderKey,
|
||||
streamFirstErrors: map[string]error{"deepseek-v3.1": invalidErr},
|
||||
}
|
||||
m := newOpenAICompatPoolTestManager(t, alias, []internalconfig.OpenAICompatibilityModel{
|
||||
@@ -740,7 +742,7 @@ func TestManagerExecuteStream_OpenAICompatAliasPoolStopsOnInvalidBootstrap(t *te
|
||||
{Name: "glm-5", Alias: alias},
|
||||
}, executor)
|
||||
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{"pool"}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
streamResult, err := m.ExecuteStream(context.Background(), []string{openAICompatPoolProviderKey}, cliproxyexecutor.Request{Model: alias}, cliproxyexecutor.Options{})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid request error")
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ func (s *authScheduler) upsertAuthLocked(auth *Auth, now time.Time) {
|
||||
return
|
||||
}
|
||||
authID := strings.TrimSpace(auth.ID)
|
||||
providerKey := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(auth)
|
||||
if authID == "" || providerKey == "" || auth.Disabled {
|
||||
s.removeAuthLocked(authID)
|
||||
return
|
||||
@@ -538,7 +538,7 @@ func (s *authScheduler) ensureProviderLocked(providerKey string) *providerSchedu
|
||||
|
||||
// buildScheduledAuthMeta extracts the scheduling metadata needed for shard bookkeeping.
|
||||
func buildScheduledAuthMeta(auth *Auth) *scheduledAuthMeta {
|
||||
providerKey := strings.ToLower(strings.TrimSpace(auth.Provider))
|
||||
providerKey := executorKeyFromAuth(auth)
|
||||
return &scheduledAuthMeta{
|
||||
auth: auth,
|
||||
providerKey: providerKey,
|
||||
|
||||
@@ -787,11 +787,16 @@ func openAICompatInfoFromAuth(a *coreauth.Auth) (providerKey string, compatName
|
||||
if providerKey == "" {
|
||||
providerKey = compatName
|
||||
}
|
||||
return strings.ToLower(providerKey), compatName, true
|
||||
return util.OpenAICompatibleProviderKey(providerKey), compatName, true
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(a.Provider), "openai-compatibility") {
|
||||
return "openai-compatibility", strings.TrimSpace(a.Label), true
|
||||
compatName = strings.TrimSpace(a.Label)
|
||||
providerKey = compatName
|
||||
if providerKey == "" {
|
||||
providerKey = "openai-compatibility"
|
||||
}
|
||||
return util.OpenAICompatibleProviderKey(providerKey), compatName, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/pluginhost"
|
||||
runtimeexecutor "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
|
||||
@@ -98,3 +99,64 @@ func TestRegisterAvailableExecutors(t *testing.T) {
|
||||
t.Fatalf("executor type = %T, want serviceTestPluginExecutor", resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterExecutorForAuth_OpenAICompatUsesNamespacedProviderKey(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
auths []*coreauth.Auth
|
||||
}{
|
||||
{
|
||||
name: "native first",
|
||||
auths: []*coreauth.Auth{
|
||||
{ID: "native-kimi", Provider: "kimi"},
|
||||
openAICompatKimiAuth(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "compat first",
|
||||
auths: []*coreauth.Auth{
|
||||
openAICompatKimiAuth(),
|
||||
{ID: "native-kimi", Provider: "kimi"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := &Service{
|
||||
cfg: &config.Config{},
|
||||
coreManager: coreauth.NewManager(nil, nil, nil),
|
||||
}
|
||||
|
||||
service.registerExecutorsForAuths(tt.auths, true)
|
||||
|
||||
nativeExecutor, okNative := service.coreManager.Executor("kimi")
|
||||
if !okNative {
|
||||
t.Fatal("expected native kimi executor")
|
||||
}
|
||||
if _, okKimi := nativeExecutor.(*runtimeexecutor.KimiExecutor); !okKimi {
|
||||
t.Fatalf("native executor type = %T, want *executor.KimiExecutor", nativeExecutor)
|
||||
}
|
||||
|
||||
compatExecutor, okCompat := service.coreManager.Executor("openai-compatible-kimi")
|
||||
if !okCompat {
|
||||
t.Fatal("expected namespaced OpenAI-compatible executor")
|
||||
}
|
||||
if _, okOpenAICompat := compatExecutor.(*runtimeexecutor.OpenAICompatExecutor); !okOpenAICompat {
|
||||
t.Fatalf("compat executor type = %T, want *executor.OpenAICompatExecutor", compatExecutor)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func openAICompatKimiAuth() *coreauth.Auth {
|
||||
return &coreauth.Auth{
|
||||
ID: "compat-kimi",
|
||||
Provider: "openai-compatibility",
|
||||
Label: "kimi",
|
||||
Attributes: map[string]string{
|
||||
"compat_name": "kimi",
|
||||
"provider_key": "kimi",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user