Skip to content

fix(api): serve Muse Code's model catalog at /muse-code/models (HOUSE-114) - #30

Merged
mergify[bot] merged 1 commit into
mainfrom
HOUSE-114-lane
Nov 6, 2026
Merged

mergify[bot] merged 1 commit into
mainfrom
HOUSE-114-lane

Conversation

@ericlitman

@ericlitman ericlitman commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

 setupRoutes
   /v1/*            AuthMiddleware
   /backend-api/codex/*
+  GET /muse-code/models   AuthMiddleware → museCodeModels
+    pick lowest-ID enabled Meta auth with a token (helps.MetaCreds)
+    GET <scheme://host of its base_url>/muse-code/models, Bearer <Meta key>
+    return upstream status, content type and body unchanged
+    no usable Meta auth → 503; transport/read failure → 502
 internal/runtime/executor/
-├── meta_executor.go        # metaCreds
+├── meta_executor.go        # calls helps.MetaCreds
 └── helps/
+    └── meta_credentials.go # MetaCreds, moved unchanged

Muse Code pointed at the proxy fetches its model catalog from <origin>/muse-code/models. The proxy had no such route, so hosts without a cached catalog could not start Muse Code (HOUSE-113).

Evidence

  • Before: the deployed proxy (v8.0.44-mob.4, a9b5081) on studio2: GET /muse-code/models with the client key → 404. studio1 with no cached catalog: muse exec → failed to fetch model catalog: API error 404: empty response body.
  • After: this branch (21eb29dd434ef9cc26c5021ac43eeced1034b621) built and run on studio2 at port 18317 with only the live Meta credential:
    • with the client key → 200, Meta's real catalog: muse-wonder-full-1.0-contributor muse-wonder-full-1.0 muse-wonder-1.0-contributor muse-wonder-1.0 muse-spark-1.3-contributor muse-spark-1.3 muse-image-1.0 muse-spark-1.2-contributor muse-spark-1.2
    • without a key → 401
  • TestMuseCodeModelsRoute (table test through the registered route, fake upstream): catalog bytes and status (418) forwarded, upstream saw /muse-code/models with no query and the Meta bearer; no Meta credential → 503; no client key → 401 with no upstream call. All three cases fail with 404 on main.

Merge Danger

Door: two-way

A new route and a moved function; reverting removes both.

Blast Radius: small

Only GET /muse-code/models is new. Meta model requests now resolve credentials through helps.MetaCreds, a byte-for-byte move of metaCreds (same body, renamed).

Acceptance criteria

GET /muse-code/models on the proxy, with a valid client API key, returns Meta's catalog body and status, using an enabled Meta credential's base origin and key.

Met: live 200 with Meta's catalog above; unit case proxies_catalog.

No Meta credential: a clear 503-class error, not 404.

Met: unit case no_meta_credential → 503 {"error":"no usable Meta credential for Muse Code models"}.

Client auth is required as for other proxied routes.

Met: AuthMiddleware(s.accessManager); live 401 without a key; unit case missing_client_key.

Unit test for the route; gofmt, go build and go test pass.

Met with one exception noted below.

After deploy to studio2: delete the cached catalog on studio1, and muse exec starts and answers; studio2's log shows GET /muse-code/models 200.

Pending deploy after merge.

Gate results (head 21eb29dd434ef9cc26c5021ac43eeced1034b621, run by the orchestrator on studio2)

Check Result
gofmt -l on changed files exit 0, no output
go build -o test-output ./cmd/server exit 0
go vet ./internal/api/ ./internal/runtime/executor/... exit 0
go test -count=1 ./... exit 1: 101 packages ok; sdk/cliproxy TestServiceHeartbeatFailoverDoesNotDrainBlockingScope failed. The same test fails 2 of 10 runs on unchanged main (a9b5081), so it is a pre-existing flake unrelated to this diff.

Not verified

  • A Meta credential that holds only a DCA token (no minted API key) gets 503; minting on this route is out of scope.
  • The live Muse Code start on studio1 without a cached catalog waits for the post-merge deploy.
  • Only comment added: the doc comment on exported helps.MetaCreds; no other comments to review.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

…-114)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@unfret

unfret Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

✅ Accepted risk

Accepted by ericlitman: HOUSE-114 (authority 48fa77b)

🟡 Medium · Redirects replace the upstream status and body

🟡 Medium · Catalog requests ignore configured Meta headers

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

Details

Run: GET /unfret/run/panel:21eb29dd434ef9cc26c5021ac43eeced1034b621:XCvu7jjBDRs:D2UqiHneGwQ.

@unfret review · @unfret status · @unfret help

@unfret unfret Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛑 3 blocking findings
🟡 Medium · Redirects replace the upstream status and body
🟡 Medium · Catalog requests ignore configured Meta headers
🟡 Medium · Catalog route pins lowest-ID Meta credential, ignoring its health
Check

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.

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 · 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:

Comment on lines +69 to +77
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

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.

@ericlitman

Copy link
Copy Markdown
Contributor Author

Waiver for head 21eb29d (HOUSE-114). All three blocking findings are pre-accepted: their triggers lie outside our actual usage, and none involves security, secret exposure, data loss, or gate integrity.

  • Redirects replace the upstream status and body (v1:2881592611…): needs api.meta.ai to redirect /muse-code/models. A live call on 2026-11-06 returned 200 JSON directly.
  • Catalog requests ignore configured Meta headers (v1:8261832d6c…): needs a Meta credential with header:* attributes. The deployed proxy's only Meta credential has none, and its config defines no Meta API keys.
  • Catalog route pins lowest-ID Meta credential (v1:b77c855f27…): needs two or more Meta credentials with the lowest-ID one broken. The deployed proxy has exactly one Meta credential, and the failure surfaces as Meta's own error status.

If any of these occurs in production, it becomes a ticket fixed with a class rule.

@mergify
mergify Bot merged commit 7b3b9fa into main Nov 6, 2026
8 of 9 checks passed
@mergify

mergify Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

✅ Merged into main as 7b3b9fa at 14:14 UTC

Less than a minute in the queue, with no time running CI.

So far

  • ✅ Entered the queue Fri 6 Nov, 14:14 · rule default · triggered by merge protections
  • ✅ Checks skipped · PR is already up-to-date
  • ✅ Merged 14:14 · 7b3b9fa
10 merge conditions met
  • github-review-approved · required by GitHub repository ruleset rule Mergify merge protections
  • -draft · required by Merge Protections rule Required checks
  • -label = hold · required by Merge Protections rule Required checks
  • check-success = Analyze (actions) · required by Merge Protections rule Required checks
  • check-success = Analyze (go) · required by Merge Protections rule Required checks
  • check-success = Analyze (javascript-typescript) · required by Merge Protections rule Required checks
  • check-success = e2e-hermetic · required by Merge Protections rule Required checks
  • check-success = go · required by Merge Protections rule Required checks
  • check-success = @unfret/Unfret · required by Merge Protections rule Unfret on risky paths
  • any of · required by GitHub repository ruleset rule Mergify merge protections:
    • check-success = @mergify/Mergify Merge Protections
    • check-neutral = @mergify/Mergify Merge Protections
    • check-skipped = @mergify/Mergify Merge Protections

Times in UTC · dashboard · how the merge queue works

@ericlitman
ericlitman deleted the HOUSE-114-lane branch November 6, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant