From 21eb29dd434ef9cc26c5021ac43eeced1034b621 Mon Sep 17 00:00:00 2001 From: "Eric A. Litman" Date: Fri, 6 Nov 2026 08:57:16 -0500 Subject: [PATCH] fix(api): serve Muse Code's model catalog at /muse-code/models (HOUSE-114) Co-Authored-By: Claude Opus 5.5 --- internal/api/server_muse_code_models.go | 81 +++++++++++++++++++ internal/api/server_muse_code_models_test.go | 77 ++++++++++++++++++ internal/api/server_routes.go | 2 + .../executor/helps/meta_credentials.go | 56 +++++++++++++ internal/runtime/executor/meta_executor.go | 61 ++------------ .../runtime/executor/meta_executor_execute.go | 4 +- .../runtime/executor/meta_executor_stream.go | 2 +- .../runtime/executor/meta_executor_test.go | 11 +-- 8 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 internal/api/server_muse_code_models.go create mode 100644 internal/api/server_muse_code_models_test.go create mode 100644 internal/runtime/executor/helps/meta_credentials.go diff --git a/internal/api/server_muse_code_models.go b/internal/api/server_muse_code_models.go new file mode 100644 index 000000000..c2f97a952 --- /dev/null +++ b/internal/api/server_muse_code_models.go @@ -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) + 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 + } + } + return chosen, chosenBaseURL, chosenToken +} diff --git a/internal/api/server_muse_code_models_test.go b/internal/api/server_muse_code_models_test.go new file mode 100644 index 000000000..675f958c9 --- /dev/null +++ b/internal/api/server_muse_code_models_test.go @@ -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) + } + }) + } +} diff --git a/internal/api/server_routes.go b/internal/api/server_routes.go index 7bf7ff70b..419d81b84 100644 --- a/internal/api/server_routes.go +++ b/internal/api/server_routes.go @@ -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)) diff --git a/internal/runtime/executor/helps/meta_credentials.go b/internal/runtime/executor/helps/meta_credentials.go new file mode 100644 index 000000000..a9329268e --- /dev/null +++ b/internal/runtime/executor/helps/meta_credentials.go @@ -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 +} diff --git a/internal/runtime/executor/meta_executor.go b/internal/runtime/executor/meta_executor.go index 34d428d9c..5efb32ebb 100644 --- a/internal/runtime/executor/meta_executor.go +++ b/internal/runtime/executor/meta_executor.go @@ -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 { @@ -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{ @@ -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 } @@ -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) != "" } @@ -209,7 +209,7 @@ 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) @@ -217,7 +217,7 @@ func (e *MetaExecutor) ensureAuth(ctx context.Context, auth *cliproxyauth.Auth) return nil, err } auth = refreshed - _, token = metaCreds(auth) + _, token = helps.MetaCreds(auth) } } @@ -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() @@ -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 diff --git a/internal/runtime/executor/meta_executor_execute.go b/internal/runtime/executor/meta_executor_execute.go index d8af85396..d1a29a854 100644 --- a/internal/runtime/executor/meta_executor_execute.go +++ b/internal/runtime/executor/meta_executor_execute.go @@ -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) @@ -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"} } diff --git a/internal/runtime/executor/meta_executor_stream.go b/internal/runtime/executor/meta_executor_stream.go index 904f30650..87399d607 100644 --- a/internal/runtime/executor/meta_executor_stream.go +++ b/internal/runtime/executor/meta_executor_stream.go @@ -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"} } diff --git a/internal/runtime/executor/meta_executor_test.go b/internal/runtime/executor/meta_executor_test.go index 1e33cc6dd..879c038c7 100644 --- a/internal/runtime/executor/meta_executor_test.go +++ b/internal/runtime/executor/meta_executor_test.go @@ -20,6 +20,7 @@ import ( metaauth "github.com/router-for-me/CLIProxyAPI/v8/internal/auth/meta" "github.com/router-for-me/CLIProxyAPI/v8/internal/config" "github.com/router-for-me/CLIProxyAPI/v8/internal/registry" + "github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor/helps" _ "github.com/router-for-me/CLIProxyAPI/v8/internal/translator" sdkauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth" @@ -46,7 +47,7 @@ func TestMetaExecutor_MetaCredsResolution(t *testing.T) { "base_url": "https://custom.meta.com/v1", }, } - baseURL, token := metaCreds(auth) + baseURL, token := helps.MetaCreds(auth) if baseURL != "https://custom.meta.com/v1" || token != "attr-key" { t.Errorf("unexpected creds: base=%s, token=%s", baseURL, token) } @@ -58,7 +59,7 @@ func TestMetaExecutor_MetaCredsResolution(t *testing.T) { "access_token": "meta-oauth-token", }, } - baseURL, token := metaCreds(auth) + baseURL, token := helps.MetaCreds(auth) if baseURL != "https://api.meta.ai/v1" || token != "meta-oauth-token" { t.Errorf("unexpected creds: base=%s, token=%s", baseURL, token) } @@ -73,7 +74,7 @@ func TestMetaExecutor_MetaCredsResolution(t *testing.T) { t.Setenv("MUSE_AUTH_PATH", authPath) // nil auth should NOT bleed to env or local file - baseURL, token := metaCreds(nil) + baseURL, token := helps.MetaCreds(nil) if token != "" { t.Errorf("expected empty token for nil auth, got %s", token) } @@ -83,7 +84,7 @@ func TestMetaExecutor_MetaCredsResolution(t *testing.T) { // empty auth should NOT bleed to env or local file emptyAuth := &cliproxyauth.Auth{} - _, token = metaCreds(emptyAuth) + _, token = helps.MetaCreds(emptyAuth) if token != "" { t.Errorf("expected empty token for empty auth, got %s", token) } @@ -552,7 +553,7 @@ func TestMetaExecutorRefreshUsesMintedBaseURL(t *testing.T) { if err != nil { t.Fatal(err) } - if got, _ := metaCreds(updated); got != tc.want { + if got, _ := helps.MetaCreds(updated); got != tc.want { t.Errorf("request base URL = %q, want %q", got, tc.want) } if updated.Metadata["base_url"] != tc.want {