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:
Luis Pater
2026-06-20 00:58:20 +08:00
parent c44d4fcc7c
commit 4c78e40da4
8 changed files with 193 additions and 64 deletions
+14
View File
@@ -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
+6 -4
View File
@@ -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,
+38 -1
View File
@@ -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)
+21 -14
View File
@@ -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))
}
+43 -41
View File
@@ -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")
}
+2 -2
View File
@@ -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,
+7 -2
View File
@@ -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",
},
}
}