DEV Community

Cover image for 5 Calls, 2 OpenCode Servers: 3 Threw, 1 Lied, 1 Worked.
fable7
fable7

Posted on

5 Calls, 2 OpenCode Servers: 3 Threw, 1 Lied, 1 Worked.

Two days ago I ran OpenCode's opencode upgrade --major against a throwaway HOME and wrote up which of the three breaking changes actually fires. The server API was the one I had to skip — I had no client integration in that test, so I said exactly that instead of guessing.

This is the follow-up: the same @opencode/client on the same machine, pointed at a v2 server and then at a v1 server, five calls each, everything logged.

One client package, two servers: what each call did

The setup

Two servers, both local, both fed the same big-pickle model:

# v2: 2.0.28, and it prints a server password on boot
HOME=/tmp/oc2test/home2 opencode2 serve --port 7911
# server listening on http://127.0.0.1:7911
# server password <32 random chars>

# v1: 1.19.0, no auth at all
HOME=/tmp/oc2test/home opencode serve --port 7912
# Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.
Enter fullscreen mode Exit fullscreen mode

One client:

npm install @opencode/client@2.0.28
Enter fullscreen mode Exit fullscreen mode

Then one script, /tmp/oc2test/api/probe-v2-client.mjs, doing OpenCode.make({ baseUrl, headers }) → server.info → model.list → session.create → session.prompt → poll message.list. Same file, two TARGET_BASE values.

What each call actually did

server.info — v2 returns {"version":"2.0.28","pid":195571,"capabilities":{"persistentPty":true}}. On v1 the client throws UnsupportedContentType: text/html, because v1 has no /api/info route and the request falls through to the Web UI page. A 200-looking HTML page where JSON was expected.

model.list — this is the interesting one. Against v2: 12 models, objects shaped { modelID, settings, package, compatibility }. Against v1: the client parses 0 models and throws nothing. Raw curl on the same v1 path returns 39 models. The shape is { id, providerID, api, request }, so a v2 client walking modelID finds nothing and quietly reports an empty list. A silent empty result is worse than an exception: your loop just does nothing.

session.create — the only call that passed unchanged on both. v2 gives ses_... with a 40-char hex projectID; v1 gives ses_... with the literal string "global". Compatible enough to lull you.

session.prompt — v2 accepts { sessionID, text } and the assistant replied pong in about 5 seconds. v1 throws:

Missing key
  at ["prompt"]
Enter fullscreen mode Exit fullscreen mode

Because v1's /api/session/{id}/prompt requires {"prompt": PromptInput} while v2 requires {"text": string} with additionalProperties: false. One field name, whole call dead.

message.list — v2 returns the message stream (user, assistant, idle) with the assistant text in content[].text. On v1 it returned [] on every poll, because the prompt never landed.

The auth change, which bites before any of that

The v2 server enforces HTTP Basic on everything: no header, or a wrong password, gets you 401 {"_tag":"UnauthorizedError","message":"Authentication required"}. The username is ignored; the password is whatever the server printed, and the CLI reveals the variable name it reads — OPENCODE_PASSWORD.

The v1.19.0 server has no auth. It logs one warning about OPENCODE_SERVER_PASSWORD not being set and then serves anybody who can reach the port. If you're migrating, this is the line in your scripts to check first, and it's a bigger deal than the field names: a v1 server bound to anything but loopback is an open door to an agent that runs shell commands. (There was a remote-code-execution in the /global/upgrade endpoint, written up by Datadog and patched in August 2026 — a reminder of what an unauthenticated control port is worth.)

"v1 has /api/*" is a trap

I dumped both OpenAPI documents to compare surfaces:

v2.0.28: 118 paths / 142 operations, all under /api/*
v1.19.0: 162 paths / 188 operations — legacy /session/* AND a /api/*
         surface whose operationIds are already prefixed "v2."
Enter fullscreen mode Exit fullscreen mode

So v1.19.0 shipped with a pre-seeded /api surface. That's why session.create works from a v2 client, and it's also why "the route exists, we're fine" is the wrong test. The routes exist; the request bodies don't match. The official migrate-from-V1 guide says the server API and clients "have new contracts" — this is what that sentence costs at the call site.

The check I'd run before touching client code

# 1. Is the server even there, and which version?
curl -su ":${OPENCODE_PASSWORD}" http://127.0.0.1:7911/api/info

# 2. What body does this server want for prompt?
curl -su ":${OPENCODE_PASSWORD}" http://127.0.0.1:7911/openapi.json \
  | grep -o '"prompt"[^,]*' | head -3     # v2: no "prompt" key. v1: required.
Enter fullscreen mode Exit fullscreen mode

Two commands, on the target machine, before a migration branch. The rest of the port is mechanical once you know the two contract shapes.

Verdict

OpenCode 2 isn't a version bump for integrations, it's a different backend with a familiar package name. My measured split from five calls: three throw for the wrong reason or return nothing, one returns an empty list that looks like success, one works.

If a third-party project has already paid this bill, their changelog is a shortcut. The ai-sdk-provider-opencode-sdk provider, for instance, pins @opencode/client@2.0.24 and documents the move as "a new backend, not an SDK bump", including the Basic-auth requirement and the removal of the { data, error } field style in favour of thrown typed errors. That matches what I hit from the other direction: same client, different server, very different behaviour.

The v2 runtime itself was boring in the good way — session, prompt, streaming reply, tokens, cost, all in one file and one pass. The migration pain is entirely in the seam between the two contracts.

Everything above is from the local run on 2026-10-13: /tmp/oc2test/api/ holds the probe script, both run logs, both OpenAPI dumps, and the raw v1/v2 messages. Sources: the official V2 migration and plugin-migration guides, the Anomaly 2.0 notes, The New Stack's write-up of the 2.0 release, Datadog Security Labs on GHSA-632h-h47v-g4x4.

Top comments (0)