anomalyco / anomalyco/opencode
OpenCode server URLs drop full path in v2
@jlongster is already working on this.
Since Aug 31, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- Avg merge
- 7h 2m
- Merged PRs (30d)
- 384
Description
Description
OpenCode allows you to attach a client to an arbitrary server url. In OpenCode v1, I was able to attach it to a url like this: http://127.0.0.1:3000/proxy and it would work fine. In v2, there is a URL parsing bug that drops the /proxy part and treats your inputted server url of http://127.0.0.1:3000/proxy as if it was http://127.0.0.1:3000.
Below is the AI generated analysis of the problem including the spots it needs a fix. Apologies for not hand writing everything, but I think it is a relatively small and straightforwards bug + repro
Repro repo aidansunbury/v1v2-path-proxy-repro
OpenCode v2 drops the path prefix of a --server URL
Verified with the installed binaries:
opencode(v1): 1.18.25opencode2(v2): v0.0.0-beta-18743
Summary
A server mounted under a path prefix works with the v1 client but
fails with the v2 client. The v2 remote client resolves its root-relative
/api/... paths with new URL(path, serverUrl), and a leading / replaces the
whole path component of serverUrl — so the configured prefix is discarded.
new URL("/api/health", "http://127.0.0.1:3001/proxy").toString()
// => http://127.0.0.1:3001/api/health (the /proxy prefix is gone)
Reproduction
repro.sh starts a real v1 server and a real v2 server, then path-proxy.ts
(one bun process on :3000) mounts each under a path prefix, with a non-opencode
root like Forge:
opencode serve :4096 (v1, unsecured) proxy :3000/v1/* -> v1 server
opencode2 serve :4097 (v2, password) proxy :3000/v2/* -> v2 server
proxy :3000/... -> "running" (not opencode)
Each client is then pointed at its server directly (baseline) and through
the path proxy. Run ./repro.sh.
Result
| client (→ its server) | direct | through path proxy |
|---|---|---|
opencode (v1) → v1 server |
✅ | ✅ …/v1/* (prefix kept) |
opencode2 (v2) → v2 server |
✅ | ❌ fails (prefix dropped) |
Both servers are reachable through the proxy (verified by curl:
/v1/global/health and /v2/api/health both return healthy). Both clients work
directly. Only the v2 client fails once its URL carries a path prefix.
v2 client — direct works, path fails
$ OPENCODE_SERVER_PASSWORD=… opencode2 api --server http://127.0.0.1:4097 GET /api/health
{"healthy":true,"version":"0.0.0-beta-18743","pid":38601} # direct: OK
$ OPENCODE_SERVER_PASSWORD=… opencode2 api --server http://127.0.0.1:3000/v2 GET /api/health
ERROR: Server at http://127.0.0.1:3000/v2 did not provide a compatible V2 health response
[cause]: ClientError: UnsupportedContentType # path: FAIL
The proxy access log for the failing run shows GET /api/health — the /v2
prefix was dropped, so it hit the non-opencode root, which returns running and
is rejected. (The direct call proves the v2 server, client, and auth are all
fine; only the path prefix differs.)
v1 client — path proxy keeps the prefix
opencode attach http://127.0.0.1:3000/v1 connects; every request stays under
the prefix:
GET /v1/path
GET /v1/config/providers
GET /v1/global/event <-- even the event stream
...
The v1 client joins base + path by concatenation
(baseUrl + (path.startsWith("/") ? path : "/"+path)), preserving the prefix.
Source comparison
Branch: dev (the v2 line; the -v2 remote branches are feature branches)
Commit: 04284921ac (latest origin/dev at time of writing)
V1 — string concatenation (preserves the prefix)
packages/sdk/js/src/gen/core/utils.gen.ts:96-97
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
let url = (baseUrl ?? "") + pathUrl
baseUrl = "http://127.0.0.1:3001/proxy", _url = "/api/health" →
http://127.0.0.1:3001/proxy/api/health. The prefix survives.
V2 — URL resolution (drops the prefix)
packages/app/src/utils/server-protocol.ts:14
const response = await fetch(new URL(path, server.url), {
called with root-relative paths at packages/app/src/utils/server-protocol.ts:28
and :31:
const legacy = await probe(server, fetch, "/global/health")... // line 28
const current = await probe(server, fetch, "/api/health")... // line 31
new URL("/api/health", "http://127.0.0.1:3001/proxy") →
http://127.0.0.1:3001/api/health. The leading / replaces the path component,
so /proxy is discarded.
Introduced by commit d03e0c5e547f2bc7ae44e60eb21bfb24dad623fd
("feat(app): add dual-server compatibility", #38462).
It is broader than the probe: the main v2 client drops the prefix too
There are two v2 client generators, and the app uses both:
@opencode-ai/sdk/v2— gen builder, concatenates → keeps the prefix
(packages/sdk/js/src/v2/gen/core/utils.gen.ts:96-97). Used by
createSdkForServer(packages/app/src/utils/server.ts:34).@opencode-ai/client(OpenCode.make) — resolves withnew URL→ drops
the prefix. This is the client used for health checks and the main API.
packages/client/src/generated/client.ts:
144: const url = new URL(descriptor.path, options.baseUrl) // origin-relative
253: health: { … path: `/api/health` … } // all 61 endpoints are /api/...
new URL("/api/health", ".../proxy") → .../api/health, and the same for every
endpoint (/api/session, /api/agent, …). When the origin returns non-JSON
(Forge's running), this client raises UnsupportedContentType — the exact
cause in the observed CLI error.
Prefix-losing call sites, all resolving /api/... against the origin:
packages/app/src/utils/server-protocol.ts:14— protocol detection probepackages/app/src/utils/server-health.ts—checkServerHealth, viaOpenCode.makepackages/app/src/utils/server.ts:48—createApiForServer, viaOpenCode.make
The only prefix-preserving client (@opencode-ai/sdk/v2, string concat) is not
the one doing health/detection/API. So the trailing path is dropped fundamentally,
not just during health checks.
Where it runs (both cases reproduce)
- Web UI — add/edit server:
packages/app/src/components/dialog-select-server.tsx:262
(checkServerHealth) and:269(detectServerProtocol); edit path at:313/:320. - Connection context (web + v2 CLI/TUI):
packages/app/src/context/server-sdk.tsx:208
(detectServerProtocol). Theapppackage is shared by the web front end and the v2 CLI.
Fix sites (exhaustive) and V1 status
An audit of every new URL(), fetch(), WebSocket, and EventSource
request-builder in packages/client and packages/app finds exactly two
that resolve endpoint paths against the origin (dropping the prefix). Both are
new in V2; the V1 equivalents do not have the bug — which is why V1 never broke.
| # | V2 fix site | Scope | V1 status |
|---|---|---|---|
| 1 | packages/httpapi-codegen/src/index.ts:537 (codegen template) → generated packages/client/src/generated/client.ts:144 — new URL(descriptor.path, options.baseUrl) |
All 61 REST + SSE endpoints of @opencode-ai/client (health, sessions, agents, events) |
No bug. V1 uses a different, older client — @opencode-ai/sdk, packages/sdk/js/src/gen/core/utils.gen.ts:97 → (baseUrl ?? "") + pathUrl (concat, keeps prefix; created 2025-08-22). The new URL client is a V2 addition (2026-06-24, #33445). |
| 2 | packages/app/src/utils/server-protocol.ts:14 — fetch(new URL(path, server.url)) |
Protocol-detection probe | Did not exist in V1 — added for V2 (2026-07-23, #38462). |
Site 1 is a generated file, so fix the codegen template (then regenerate);
that one change covers every endpoint of @opencode-ai/client, including the
SSE event stream (which flows through the same prepare() → new URL).
The fix
Resolve endpoint paths relative to the configured base instead of against the
origin — normalize the base to end in / and drop the path's leading slash:
function apiUrl(base: string | URL, path: string) {
const b = new URL(base)
if (!b.pathname.endsWith("/")) b.pathname += "/"
return new URL(path.replace(/^\//, ""), b) // relative join, prefix preserved
}
// apiUrl("http://127.0.0.1:3001/proxy", "/api/health") => .../proxy/api/health
// apiUrl("http://127.0.0.1:3001", "/api/health") => .../api/health (no regression)
Not fix sites (verified correct or unrelated)
packages/sdk/js/src/v2/gen/core/utils.gen.ts:96-97— v2 gen SDK, concatenates (safe).packages/app/src/utils/terminal-websocket-url.ts:16— concatenates, already
handles v1/v2 (safe).server-sdk.tsx:194,entry.tsx:85,terminal.tsx:186/385/620,
home-projects-view.tsx:253,deep-links.ts:7,draft-store.ts:157—
single-arg URL parsing, display, same-origin checks, or blob URLs; not request
construction fromserver.url.
Plugins
No response
OpenCode version
v0.0.0-beta-18743
Steps to reproduce
https://github.com/aidansunbury/v1v2-path-proxy-repro
This repo gives a super quick repro setup, and a script to run
Screenshot and/or share link
No response
Operating System
Mac OS
Terminal
Ghostty
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.