Repository navigation
Description
Facade routes are in the OpenAPI spec but not served by opencode serve (SPA/405)
Found during the #1146 deploy-gated sequence on generation 528 (2026-11-04). The #1146 facades answer in tests, but the deployed serve listener does not route them:
Evidence (running server, 127.0.0.1:13420, build 2.0.30+4175c63, pid 616146)
- GET /openapi.json (basic auth) lists all facade paths: /v1/chat/completions, /v1/responses, /v1/embeddings, /v1/messages, /v1/systemone, /v1/models, /v1/models/{id}, /v1/models/{provider}/{model} (140 paths total).
- GET /v1/models (with and without basic auth, with x-opencode-directory header) returns 200 with the web UI SPA HTML, not the models JSON.
- POST /v1/chat/completions and POST /v1/responses (with/without auth, with directory header and ?directory= query) return 405 with empty body.
- /api/* endpoints on the same listener work normally with the same auth (e.g. GET /api/credential returns data).
- The binary contains the completions group (server.completions, completions.models strings present).
Reproduced on the pinned build too
A scratch instance of the pinned build (opencode-2.0.30+ae5d592, the flake.lock opencode_2 rev ae5d592) on a scratch port/HOME/DB behaves identically: SPA HTML for GET /v1/models, 405 for POST /v1/chat/completions. So this is not a stale-build artifact: at both revs, packages/server/src/handlers.ts registers CompletionsHandler and routes.ts mounts HttpApiBuilder.layer(Api) unconditionally, yet the serve listener does not route the group. The OpenAPI is generated from the Api definition, which is why the spec advertises routes the listener does not serve. Suspect the serve entry router composition (UI catch-all shadowing the API router for GET, no fall-through for POST), while the fetch-handler entry used in tests serves the API only — worth checking where serve composes its router versus ServerFetch.make.
Impact
Blocks the #1146 migration legs that repoint callers to the facade: #1410 (clef to /v1/systemone), #1153 (route-proxy retirement + OpenClaw rewire proof), the #1155 live attestation probe, and the facade leg of the #1154 Gate 1 verification window. Session traffic is unaffected.
Acceptance criteria
- Every route advertised in the OpenAPI spec is either served by
opencode serveor removed from the spec - The facade /v1/* behavior (SPA fallback / 405) is intentional and documented for any route kept in the spec
- A test or scripted check compares spec routes against served routes
Activity
dezren39 commentedon Nov 4, 2026
Intake review
Merge pull request #1638 from developing-today-forks/fix/1627-facade-…
dezren39 commentedon Nov 4, 2026
Fixed — root cause, fix, and scratch-instance proof
Root cause. The façade handlers and routes were never the problem.
opencode servecomposes its listener asWebUi.handler()wrapped around the API router (thetransformargument ofServerProcess.start, built inpackages/cli/src/server-process.ts). The transform inpackages/cli/src/services/web-ui.tspassed only/api,/api/*,/auth/*and/openapi.jsonthrough to the router and answered every other path from the embedded web shell: GETs fell back to the SPAindex.html(200 text/html), and non-GET/HEAD requests got the shell's bare 405. The completions group is the only API group outside/api— all eight façade paths live under/v1— so the serve listener advertised the doors in its OpenAPI spec (generated from theApidefinition) but routed none of them. The fetch-handler entry (ServerFetch.make, used by tests and the workerd/embedded entries) applies no transform, which is why the façade answered there whileservedid not. This matches the issue's suspicion exactly: serve-entry router composition, not route registration.Fix. PR #1638 (merged,
63c50eec0d27139b6ebdc57ed85d4d3ace35b5b3): the WebUi transform now passes/v1and/v1/*through to the API router alongside/api. One condition added, no other behavior change — the shell still owns every non-API path, and unknown/v1paths now 404 through the router pass-through instead of serving the SPA. Regression coverage inpackages/cli/test/web-ui.test.ts(the spec-vs-served check for this composition): the liveServerProcesstest now asserts unauthenticated/v1/*meets the API auth gate, authenticated façade requests return façade JSON (models list, chat 400invalid_request_error, systemone{error}envelope, embeddings JSON), and the stub-API tests cover/v1pass-through with the UI disabled and/v1/missing→ 404. 4/4 tests pass (116 assertions);tsgo -btypecheck in packages/cli passes; oxlint clean.Scratch-instance proof (fixed build from source, own port 13499, own data dir/DB, the deployed server's config overlay; plugin list emptied in the scratch config copy only, because a cold plugin cache cannot finish activation inside
awaitPlugins' 5-second window — the deployed server's warm cache is unaffected):GET /→ 200 text/html (SPA shell intact)GET /v1/modelswithout auth → 401 application/json (the API auth gate, not the shell)GET /v1/modelswith auth → 200 JSON: the full route catalog (deepseek-flash-plan,haiku-plan,terra-plan,sonnet-plan,opus-plan, … owned_byopencode-route) plus the embeddings entrylocal-embed/snowflake-arctic-embed-m-v2.0GET /v1/models/deepseek-flash-plan→ 200 JSON single-model objectPOST /v1/chat/completionswith a real route (deepseek-flash-plan, one-line prompt) → 502 JSONserver_error— the request went through route resolution and failover and was ledgered: 12route_attemptrows in the scratch DB withsession_idNULL (the façade convention), including attempts onopendesignandantigravity-direct. It did not complete because the scratch instance has an empty credential store (and the antigravity client secret is separately broken today, antigravity: client-secret extraction (first GOCSPX match) stale for agy 1.4.11 — refresh fails invalid_client #1633) — a well-formed routing error from inside Generate, not a 405 and not the SPA.POST /v1/responses(unknown model) → 400 JSON{"error":{"message":"Model unavailable: opencode-route/no-such-model","type":"invalid_request_error"}}POST /v1/messages(unknown model) → 400 JSON in the Anthropic error shape, same messagePOST /v1/systemonewith the recorded clef classifier probe (the exact body transcribed inpackages/core/test/facade-parity.test.ts) → 401 with the door's upstream-failure envelope, and aroute_attemptrow (providercloudflare-ai-gateway, model@cf/cloudflare/clef-flash). Parity check: the same probe sent to the still-running standalone door at127.0.0.1:18742/v1/systemonereturned the byte-identical response (same envelope, same Cloudflarecode 10000"Authentication error", same 401). The upstream authentication failure is a pre-existing credential condition that currently affects the door and the façade identically; the façade's systemone is behaviorally indistinguishable from the door for this request.POST /v1/embeddings→ 502 JSON{"error":{"message":"Transport error (POST http://127.0.0.1:18752/v1/embeddings)","type":"server_error"}}, withroute_attemptrows forlocal-embed. State of the embeddings door: the route is registered and served by the façade; its backend is still the HTTP sidecar registration (nothing listens on :18752, per step 5's gate), so requests answer a well-formed transport error. The Embeddings: bake the local backend into the fork (native in-process), retire the :18752 sidecar #1554 native-onnx flip is untouched by this change, as instructed.Deployment note. A plain restart of the running server does not suffice: the deployed binary is the nix build of the currently pinned fork rev, which predates this fix. The running server carries the fix only after the code repo's flake pin for the fork advances to a rev containing
63c50eecand the server is redeployed/restarted on the new build (a switch round, or a targeted redeploy of the server package plus a restart of the login-session server process). The switch round in flight at the time of this fix still uses the old pin.Unblocks (follow-ons not performed here, per the sequence's ownership):
classifier-endpoint.jsonurl →http://127.0.0.1:13420/v1/systemone) can proceed once the deployed server carries the fix; the parity check is unblocked (expect the current upstream Cloudflare auth failure to appear identically on both sides until that credential is repaired).127.0.0.1:13420/v1) four-part proof can be re-attempted on the redeployed server; route-proxy retirement stays behind that proof, as designed.developing-today commentedon Nov 4, 2026
Live verification on the deployed server — the redeploy this issue was held open for
The fix is now live on the standing server. Path: code PR anomalyco#5567 advanced the fork pin to 63c50ee (and, via
inputs.opencode.follows, ended the cascade float — code#5519), merged as 4d2eeecb; a switch on 2026-11-04 (~10:36 CST, SWITCH_EXIT=0) cycledopencode.serviceonto the pinned build. Verified live, in order:XDG_CACHE_HOME, the evaluated server package readsopencode-2.0.30+63c50ee-wrappedandflake.lockis byte-identical before/after evaluation (sha256 ebafe50b…) — the float no longer re-resolves anything./nix/store/m00436…-opencode-2.0.30+63c50ee-wrapped/bin/opencode serve --service --hostname 127.0.0.1 --port 13420; the process exe resolves to/nix/store/4r20ria3…-opencode-2.0.30+63c50ee/bin/.opencode-wrapped— exactly the outPath the PR's scratch verification built.GET /→ 200 text/html (SPA intact)GET /v1/modelswithout auth → 401 JSONUnauthorizedErrorGET /v1/modelswith auth → 200 JSON, 28 models, includinglocal-embed/snowflake-arctic-embed-m-v2.0POST /v1/systemone(recorded clef probe) → 401 in the door envelope — byte-identical to the still-running :18742 door's response in the same run (Cloudflare code 10000; that credential is now tracked separately as fix(providers): cloudflare-ai-gateway credential fails upstream auth (code 10000), blocking clef classifications #1651)POST /v1/chat/completions(terra-plan) → 200, a real completion through the façadePOST /v1/embeddings→ 502 well-formed transport-error envelope (the :18752 sidecar remains down behind the Embeddings: bake the local backend into the fork (native in-process), retire the :18752 sidecar #1554 gate, as designed)route_attemptrows withsession_idNULL (façade convention)The issue was closed by @dezren39 earlier today; this comment supplies the post-redeploy live evidence the close was waiting on. The unblocks recorded here have since executed: #1410 (repoint + door retirement) and the #1146 step-3 OpenClaw rewire behind #1153 — evidence on those issues.