From 2eef6875e94bd0587247b92c35270f501f926ac9 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 3 Oct 2025 02:38:30 +0800 Subject: [PATCH] feat(auth): improve OpenAI compatibility normalization and API key handling - Refined trimming and normalization logic for `baseURL` and `apiKey` attributes. - Updated `Authorization` header logic to omit empty API keys. - Enhanced compatibility processing by handling empty `api-key-entries`. - Improved legacy format fallback and added safeguards for empty credentials across executor paths. --- .../executor/openai_compat_executor.go | 20 ++++++----- internal/watcher/watcher.go | 35 ++++++++++++++++--- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 7a9155b3..356681cd 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -40,8 +40,8 @@ func (e *OpenAICompatExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.A func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { baseURL, apiKey := e.resolveCredentials(auth) - if baseURL == "" || apiKey == "" { - return cliproxyexecutor.Response{}, statusErr{code: http.StatusUnauthorized, msg: "missing provider baseURL or apiKey"} + if baseURL == "" { + return cliproxyexecutor.Response{}, statusErr{code: http.StatusUnauthorized, msg: "missing provider baseURL"} } reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) @@ -60,7 +60,9 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A return cliproxyexecutor.Response{}, err } httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+apiKey) + if apiKey != "" { + httpReq.Header.Set("Authorization", "Bearer "+apiKey) + } httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat") httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) @@ -89,8 +91,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { baseURL, apiKey := e.resolveCredentials(auth) - if baseURL == "" || apiKey == "" { - return nil, statusErr{code: http.StatusUnauthorized, msg: "missing provider baseURL or apiKey"} + if baseURL == "" { + return nil, statusErr{code: http.StatusUnauthorized, msg: "missing provider baseURL"} } reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) from := opts.SourceFormat @@ -107,7 +109,9 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy return nil, err } httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+apiKey) + if apiKey != "" { + httpReq.Header.Set("Authorization", "Bearer "+apiKey) + } httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat") httpReq.Header.Set("Accept", "text/event-stream") httpReq.Header.Set("Cache-Control", "no-cache") @@ -171,8 +175,8 @@ func (e *OpenAICompatExecutor) resolveCredentials(auth *cliproxyauth.Auth) (base return "", "" } if auth.Attributes != nil { - baseURL = auth.Attributes["base_url"] - apiKey = auth.Attributes["api_key"] + baseURL = strings.TrimSpace(auth.Attributes["base_url"]) + apiKey = strings.TrimSpace(auth.Attributes["api_key"]) } return } diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index fe701b58..ae8c06e1 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -799,23 +799,23 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { base := strings.TrimSpace(compat.BaseURL) // Handle new APIKeyEntries format (preferred) + createdEntries := 0 if len(compat.APIKeyEntries) > 0 { for j := range compat.APIKeyEntries { entry := &compat.APIKeyEntries[j] key := strings.TrimSpace(entry.APIKey) - if key == "" { - continue - } proxyURL := strings.TrimSpace(entry.ProxyURL) idKind := fmt.Sprintf("openai-compatibility:%s", providerName) id, token := idGen.next(idKind, key, base, proxyURL) attrs := map[string]string{ "source": fmt.Sprintf("config:%s[%s]", providerName, token), "base_url": base, - "api_key": key, "compat_name": compat.Name, "provider_key": providerName, } + if key != "" { + attrs["api_key"] = key + } if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" { attrs["models_hash"] = hash } @@ -830,6 +830,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { UpdatedAt: now, } out = append(out, a) + createdEntries++ } } else { // Handle legacy APIKeys format for backward compatibility @@ -843,10 +844,10 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { attrs := map[string]string{ "source": fmt.Sprintf("config:%s[%s]", providerName, token), "base_url": base, - "api_key": key, "compat_name": compat.Name, "provider_key": providerName, } + attrs["api_key"] = key if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" { attrs["models_hash"] = hash } @@ -860,8 +861,32 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { UpdatedAt: now, } out = append(out, a) + createdEntries++ } } + if createdEntries == 0 { + idKind := fmt.Sprintf("openai-compatibility:%s", providerName) + id, token := idGen.next(idKind, base) + attrs := map[string]string{ + "source": fmt.Sprintf("config:%s[%s]", providerName, token), + "base_url": base, + "compat_name": compat.Name, + "provider_key": providerName, + } + if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" { + attrs["models_hash"] = hash + } + a := &coreauth.Auth{ + ID: id, + Provider: providerName, + Label: compat.Name, + Status: coreauth.StatusActive, + Attributes: attrs, + CreatedAt: now, + UpdatedAt: now, + } + out = append(out, a) + } } } // Also synthesize auth entries directly from auth files (for OAuth/file-backed providers)