anomalyco / anomalyco/opencode

OpenCode server URLs drop full path in v2

Open
#46,498 5 comments 4 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Aug 31, 2026.

2.0
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.25
  • opencode2 (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 with new 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 probe
  • packages/app/src/utils/server-health.tscheckServerHealth, via OpenCode.make
  • packages/app/src/utils/server.ts:48createApiForServer, via OpenCode.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). The app package 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:144new 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:14fetch(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 from server.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.