Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions internal/api/server_muse_code_models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package api

import (
"io"
"net/http"
"net/url"
"strings"

"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)

const museCodeModelsPath = "/muse-code/models"

func (s *Server) museCodeModels(c *gin.Context) {
credential, baseURL, token := s.museCodeCredential()
if credential == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "no usable Meta credential for Muse Code models"})
return
}

parsed, errParse := url.Parse(baseURL)
if errParse != nil || parsed.Scheme == "" || parsed.Host == "" {
log.WithField("auth_id", credential.ID).Warn("muse code models: invalid Meta base URL")
c.JSON(http.StatusBadGateway, gin.H{"error": "invalid Meta base URL"})
return
}
target := (&url.URL{Scheme: parsed.Scheme, Host: parsed.Host, Path: museCodeModelsPath}).String()

ctx := c.Request.Context()
req, errReq := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if errReq != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to build Muse Code models request"})
return
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")

resp, errDo := helps.NewProxyAwareHTTPClient(ctx, s.getConfig(), credential, 0).Do(req)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Redirects replace the upstream status and body

NewProxyAwareHTTPClient leaves CheckRedirect unset, so this call follows redirects rather than preserving the catalog endpoint's response. For example, an upstream 302 Location: /login with a JSON error becomes the login page's 200 text/html response. This was reproduced through the registered route and violates the required status/body passthrough, presenting non-catalog bytes as a successful response.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Catalog requests ignore configured Meta headers

Meta credentials support custom headers, which the normal Meta executor applies through ApplyCustomHeadersFromAttrs. This request instead sends only the hard-coded headers; NewProxyAwareHTTPClient configures transport but does not apply credential headers. For a configured Meta gateway requiring an extra authentication header, inference succeeds but /muse-code/models returns 403 and Muse Code still cannot start without a cached catalog. A route-level reproduction confirmed this difference using the same credential. Also reported for this defect: - Catalog requests drop configured credential headers (internal/api/server_muse_code_models.go:38-41): Meta credentials support custom headers through header:* attributes, which the existing Meta executor applies, but this request sends only the built-in headers. If the configured Meta origin requires an additional header such as X-Tenant-Key, normal Meta requests succeed while /muse-code/models returns 403, leaving Muse unable to start. Passing the credential to NewProxyAwareHTTPClient does not apply those headers; it only configures transport.

Also affects:

if errDo != nil {
log.WithField("auth_id", credential.ID).Warnf("muse code models: upstream request failed: %v", errDo)
c.JSON(http.StatusBadGateway, gin.H{"error": "Muse Code models upstream request failed"})
return
}
defer func() {
if errClose := resp.Body.Close(); errClose != nil {
log.Errorf("muse code models: close upstream body: %v", errClose)
}
}()

body, errRead := io.ReadAll(resp.Body)
if errRead != nil {
log.WithField("auth_id", credential.ID).Warnf("muse code models: read upstream body: %v", errRead)
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to read Muse Code models response"})
return
}
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}

func (s *Server) museCodeCredential() (*auth.Auth, string, string) {
if s.handlers == nil || s.handlers.AuthManager == nil {
return nil, "", ""
}
var chosen *auth.Auth
var chosenBaseURL, chosenToken string
for _, candidate := range s.handlers.AuthManager.List() {
if !strings.EqualFold(candidate.Provider, "meta") || candidate.Disabled || candidate.Status == auth.StatusDisabled {
continue
}
baseURL, token := helps.MetaCreds(candidate)
if token == "" {
continue
}
if chosen == nil || candidate.ID < chosen.ID {
chosen, chosenBaseURL, chosenToken = candidate, baseURL, token
Comment on lines +69 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Catalog route pins lowest-ID Meta credential, ignoring its health

museCodeCredential skips only Disabled/StatusDisabled credentials and then always picks the lexically lowest ID. It ignores the error, unavailable and cooldown state that the auth manager uses to steer model traffic away from a broken credential, and it never falls back to another candidate. Scenario: two Meta credentials. meta-a has the lowest ID and a revoked key, so the manager marks it errored and serves model requests through meta-b, which is healthy. Every GET /muse-code/models still uses meta-a and forwards the upstream 401, so Muse Code cannot fetch its catalog or start even though a usable credential exists. The same pinning causes a permanent 502 when the lowest-ID credential has a malformed base_url while others are valid.

}
}
return chosen, chosenBaseURL, chosenToken
}
77 changes: 77 additions & 0 deletions internal/api/server_muse_code_models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package api

import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"

"github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
)

func TestMuseCodeModelsRoute(t *testing.T) {
const upstreamBody = `{"object":"list","data":[{"id":"muse-spark","object":"model"}]}`

for _, testCase := range []struct {
name string
clientKey string
registerMeta bool
wantStatus int
wantBody string
wantUpstreamN int32
}{
{name: "proxies_catalog", clientKey: "test-key", registerMeta: true, wantStatus: http.StatusTeapot, wantBody: upstreamBody, wantUpstreamN: 1},
{name: "no_meta_credential", clientKey: "test-key", wantStatus: http.StatusServiceUnavailable},
{name: "missing_client_key", registerMeta: true, wantStatus: http.StatusUnauthorized},
} {
t.Run(testCase.name, func(t *testing.T) {
var upstreamCalls atomic.Int32
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamCalls.Add(1)
if r.URL.Path != museCodeModelsPath || r.URL.RawQuery != "" {
t.Errorf("upstream URL = %s, want %s", r.URL.String(), museCodeModelsPath)
}
if got := r.Header.Get("Authorization"); got != "Bearer meta-api-key" {
t.Errorf("upstream Authorization = %q, want Meta bearer", got)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTeapot)
_, _ = w.Write([]byte(upstreamBody))
}))
t.Cleanup(upstream.Close)

server := newTestServer(t)
if testCase.registerMeta {
credential := &auth.Auth{
ID: "meta-muse-code-models",
Provider: "meta",
Status: auth.StatusActive,
Attributes: map[string]string{
"api_key": "meta-api-key",
"base_url": upstream.URL + "/v1?x=1",
},
}
if _, errRegister := server.handlers.AuthManager.Register(t.Context(), credential); errRegister != nil {
t.Fatalf("register Meta credential: %v", errRegister)
}
}

req := httptest.NewRequest(http.MethodGet, museCodeModelsPath, nil)
if testCase.clientKey != "" {
req.Header.Set("Authorization", "Bearer "+testCase.clientKey)
}
recorder := httptest.NewRecorder()
server.engine.ServeHTTP(recorder, req)

if recorder.Code != testCase.wantStatus {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, testCase.wantStatus, recorder.Body.String())
}
if testCase.wantBody != "" && recorder.Body.String() != testCase.wantBody {
t.Fatalf("body = %q, want %q", recorder.Body.String(), testCase.wantBody)
}
if got := upstreamCalls.Load(); got != testCase.wantUpstreamN {
t.Fatalf("upstream calls = %d, want %d", got, testCase.wantUpstreamN)
}
})
}
}
2 changes: 2 additions & 0 deletions internal/api/server_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ func (s *Server) setupRoutes() {
codexDirect.POST("/alpha/search", s.codexAlphaSearch)
}

s.engine.GET(museCodeModelsPath, AuthMiddleware(s.accessManager), s.museCodeModels)

// Gemini compatible API routes
v1beta := s.engine.Group("/v1beta")
v1beta.Use(AuthMiddleware(s.accessManager))
Expand Down
56 changes: 56 additions & 0 deletions internal/runtime/executor/helps/meta_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package helps

import (
"strings"

metaauth "github.com/router-for-me/CLIProxyAPI/v8/internal/auth/meta"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
)

// MetaCreds resolves the Meta API base URL and the non-DCA bearer token for a Meta auth.
func MetaCreds(a *cliproxyauth.Auth) (baseURL, token string) {
baseURL = metaauth.DefaultAPIBaseURL
if a == nil {
return baseURL, ""
}

if a.Attributes != nil {
if b := strings.TrimSpace(a.Attributes["base_url"]); b != "" {
baseURL = b
}
if k := strings.TrimSpace(a.Attributes["api_key"]); k != "" && !strings.HasPrefix(k, "dca:") {
token = k
} else if t := strings.TrimSpace(a.Attributes["access_token"]); t != "" && !strings.HasPrefix(t, "dca:") {
token = t
}
}
if a.Metadata != nil {
if baseURL == metaauth.DefaultAPIBaseURL {
if b, ok := a.Metadata["base_url"].(string); ok && strings.TrimSpace(b) != "" {
baseURL = strings.TrimSpace(b)
} else if b, ok := a.Metadata["api_base_url"].(string); ok && strings.TrimSpace(b) != "" {
baseURL = strings.TrimSpace(b)
}
}
if token == "" {
if k, ok := a.Metadata["api_key"].(string); ok && strings.TrimSpace(k) != "" && !strings.HasPrefix(strings.TrimSpace(k), "dca:") {
token = strings.TrimSpace(k)
} else if t, ok := a.Metadata["access_token"].(string); ok && strings.TrimSpace(t) != "" && !strings.HasPrefix(strings.TrimSpace(t), "dca:") {
token = strings.TrimSpace(t)
}
}
}
if token == "" && a.Storage != nil {
if ms, ok := a.Storage.(*metaauth.MetaTokenStorage); ok && ms != nil {
if ms.APIKey != "" {
token = ms.APIKey
} else if ms.AccessToken != "" && !strings.HasPrefix(ms.AccessToken, "dca:") {
token = ms.AccessToken
}
if ms.BaseURL != "" && baseURL == metaauth.DefaultAPIBaseURL {
baseURL = ms.BaseURL
}
}
}
return baseURL, token
}
61 changes: 7 additions & 54 deletions internal/runtime/executor/meta_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (e *MetaExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth
if req == nil {
return nil
}
_, token := metaCreds(auth)
_, token := helps.MetaCreds(auth)
if strings.TrimSpace(token) != "" {
req.Header.Set("Authorization", "Bearer "+token)
} else {
Expand Down Expand Up @@ -95,7 +95,7 @@ func (e *MetaExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c

dcaToken := extractDCAToken(auth)
if dcaToken == "" {
if _, token := metaCreds(auth); token != "" {
if _, token := helps.MetaCreds(auth); token != "" {
return auth, nil
}
return nil, statusErr{
Expand All @@ -117,7 +117,7 @@ func (e *MetaExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c
return nil, fmt.Errorf("meta executor: mint API key returned empty key")
}

baseURL, _ := metaCreds(auth)
baseURL, _ := helps.MetaCreds(auth)
if mintedURL := strings.TrimSpace(minted.BaseURL); mintedURL != "" {
baseURL = mintedURL
}
Expand Down Expand Up @@ -188,7 +188,7 @@ func (e *MetaExecutor) ShouldPrepareRequestAuth(auth *cliproxyauth.Auth) bool {
if auth == nil || cliproxyauth.IsConfigAPIKeyAuth(auth) {
return false
}
_, token := metaCreds(auth)
_, token := helps.MetaCreds(auth)
return token == "" && extractDCAToken(auth) != ""
}

Expand All @@ -209,15 +209,15 @@ func (e *MetaExecutor) ensureAuth(ctx context.Context, auth *cliproxyauth.Auth)
}
}

_, token := metaCreds(auth)
_, token := helps.MetaCreds(auth)
if token == "" {
if dcaToken := extractDCAToken(auth); dcaToken != "" {
refreshed, err := e.Refresh(ctx, auth)
if err != nil {
return nil, err
}
auth = refreshed
_, token = metaCreds(auth)
_, token = helps.MetaCreds(auth)
}
}

Expand All @@ -238,7 +238,7 @@ func (e *MetaExecutor) ensureAuth(ctx context.Context, auth *cliproxyauth.Auth)
}

func (e *MetaExecutor) enrichAuth(auth *cliproxyauth.Auth) *cliproxyauth.Auth {
baseURL, token := metaCreds(auth)
baseURL, token := helps.MetaCreds(auth)
var cloned *cliproxyauth.Auth
if auth != nil {
cloned = auth.Clone()
Expand Down Expand Up @@ -295,53 +295,6 @@ func extractDCAToken(a *cliproxyauth.Auth) string {
return ""
}

func metaCreds(a *cliproxyauth.Auth) (baseURL, token string) {
baseURL = metaauth.DefaultAPIBaseURL
if a == nil {
return baseURL, ""
}

if a.Attributes != nil {
if b := strings.TrimSpace(a.Attributes["base_url"]); b != "" {
baseURL = b
}
if k := strings.TrimSpace(a.Attributes["api_key"]); k != "" && !strings.HasPrefix(k, "dca:") {
token = k
} else if t := strings.TrimSpace(a.Attributes["access_token"]); t != "" && !strings.HasPrefix(t, "dca:") {
token = t
}
}
if a.Metadata != nil {
if baseURL == metaauth.DefaultAPIBaseURL {
if b, ok := a.Metadata["base_url"].(string); ok && strings.TrimSpace(b) != "" {
baseURL = strings.TrimSpace(b)
} else if b, ok := a.Metadata["api_base_url"].(string); ok && strings.TrimSpace(b) != "" {
baseURL = strings.TrimSpace(b)
}
}
if token == "" {
if k, ok := a.Metadata["api_key"].(string); ok && strings.TrimSpace(k) != "" && !strings.HasPrefix(strings.TrimSpace(k), "dca:") {
token = strings.TrimSpace(k)
} else if t, ok := a.Metadata["access_token"].(string); ok && strings.TrimSpace(t) != "" && !strings.HasPrefix(strings.TrimSpace(t), "dca:") {
token = strings.TrimSpace(t)
}
}
}
if token == "" && a.Storage != nil {
if ms, ok := a.Storage.(*metaauth.MetaTokenStorage); ok && ms != nil {
if ms.APIKey != "" {
token = ms.APIKey
} else if ms.AccessToken != "" && !strings.HasPrefix(ms.AccessToken, "dca:") {
token = ms.AccessToken
}
if ms.BaseURL != "" && baseURL == metaauth.DefaultAPIBaseURL {
baseURL = ms.BaseURL
}
}
}
return baseURL, token
}

func parseMetaRetryAfter(statusCode int, errorBody []byte, now time.Time) *time.Duration {
if (statusCode != http.StatusTooManyRequests && statusCode != http.StatusNotFound) || len(errorBody) == 0 {
return nil
Expand Down
4 changes: 2 additions & 2 deletions internal/runtime/executor/meta_executor_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (e *MetaExecutor) prepareResponsesRequest(ctx context.Context, auth *clipro
return nil, errNormalizePatch
}
body = normalizeCodexInstructions(body)
baseURL, _ := metaCreds(auth)
baseURL, _ := helps.MetaCreds(auth)
body = sanitizeOpenAIResponsesReasoningEncryptedContentForMetaBaseURL(ctx, "meta executor", body, baseURL)
body = helps.SanitizeMetaWebSearchTools(body)
body = helps.NormalizeCodexToolIntegerTypes(body, opts.Headers)
Expand Down Expand Up @@ -116,7 +116,7 @@ func (e *MetaExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
defer reporter.TrackFailure(ctx, &err)
reporter.SetTranslatedReasoningEffort(prepared.body, prepared.to.String())

baseURL, token := metaCreds(enriched)
baseURL, token := helps.MetaCreds(enriched)
if strings.TrimSpace(baseURL) == "" {
return resp, statusErr{code: http.StatusUnauthorized, msg: "meta executor: missing provider baseURL"}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/runtime/executor/meta_executor_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (e *MetaExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
defer reporter.TrackFailure(ctx, &err)
reporter.SetTranslatedReasoningEffort(prepared.body, prepared.to.String())

baseURL, token := metaCreds(enriched)
baseURL, token := helps.MetaCreds(enriched)
if strings.TrimSpace(baseURL) == "" {
return nil, statusErr{code: http.StatusUnauthorized, msg: "meta executor: missing provider baseURL"}
}
Expand Down
Loading
Loading