Files
platform/docs/opencode-fork-decision.md
pastilhasandClaude Opus 5 ecb9025f8f the fork blocker was a missing credential, not an upstream bug
Andre said his terminal opencode reaches paid zen models and suggested it was simply not set
up here. Correct, and my second wrong call on this page.

The new /api pipeline has its own credential store — /api/integration and /api/credential —
separate from auth.json, which is what the CLI, opencode run and the legacy /session surface
read. Ours had none connected, so it fell back to what needs no credential: the free tier.
One POST to /api/integration/opencode/connect/key fixes it, and it survives a serve restart.
sonnet and haiku both run on the new pipeline now.

The tell I had and did not use: the configured default is big-pickle, and a session with no
model ran on ling-3.0-tiny-free INSTEAD of the default. A pipeline ignoring its configured
default cannot use it — a credential symptom, sitting in /config/providers the whole time.

So steer, queue, interrupt and the resumable per-session SSE are all available with real
models. alpha needs the same one-time connect, and the sidecar should do it at boot rather
than depend on someone having run it by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:37:07 +01:00

156 lines
9.1 KiB
Markdown

# The Phase 2 fork: reopened, and why the first answer was wrong
> **CORRECTION, same day.** An earlier version of this file concluded "the new pipeline does not
> execute, keep the subprocess". **That was wrong, and wrong for an embarrassing reason: my probe.**
> The conclusion is reversed below. The mistake is written up rather than deleted, because the shape of
> it is the useful part.
**Decision: the fork is worth taking, and it is no longer blocked.** Not started; the blocker turned out
to be a missing credential and is fixed — see RESOLVED below.
---
## What actually happened
The serve exposes a newer `/api/session/*` surface, alongside the `/session/*` one every prior document
was written against. It offers, natively, what the parity doc lists as impossible under `stdin: 'ignore'`:
`delivery: "steer" | "queue"` on `POST /prompt`, `/interrupt`, a per-session `text/event-stream` with an
`?after=` cursor, `/compact`, `prompt.files`, and `/permission` + `/question`.
I probed it, saw prompts accepted and never executed, and concluded it was an unfinished pipeline
("`session.next.*` is the tell"). Every one of those probes passed an explicit
`model: {providerID: 'opencode', id: 'claude-sonnet-4-6'}`.
**That model silently does not run on the new pipeline.** No error, no event, no assistant message — the
prompt is admitted, stored, `prompt.admitted` and `prompted` fire, and nothing else ever happens. Drop
the model field and the identical request completes normally.
So I had one broken variable in every experiment and read the result as a property of the system.
## What is actually true, measured on 1.18.16 (both machines upgraded 2026-08-10)
| Claim | Verdict |
| ----------------------------------------- | -------------------------------------------------------------------- |
| The new pipeline executes turns | **Yes** — replies normally when no model is forced |
| `delivery: "steer"` injects mid-turn | **Yes, verified** — steered a running turn, output changed to order |
| `delivery: "queue"` runs after | **Yes, verified** — two replies, "ONE" then "TWO", zero errors |
| Model selection works at all | **Yes**`POST /api/session/{id}/model` → 204, then runs on it |
| `claude-sonnet-4-6` works there | **No** — silent no-op, at create *and* via the model route |
| `claude-sonnet-4-6` works via `run --dir` | **Yes** — verified end to end the same day |
Steer and queue are exactly the two features we hand-built for Claude and that Andre called a game
changer. Having them as primitives, plus a resumable per-session cursor that mirrors officer's durable
replay, is a strong argument for migrating.
## RESOLVED — it was a missing credential, not a bug
Andre said he had a paid Zen key working in his terminal and suggested it simply was not set up here. He
was right, and this is the second wrong conclusion I reached on this page.
**The new pipeline has its own credential store, separate from `auth.json`.** `opencode run`, the CLI and
the legacy `/session` surface all read `~/.local/share/opencode/auth.json`, which holds the Zen key — so
they reach paid models. The `/api/*` surface reads integrations instead (`/api/integration`,
`/api/credential`), and ours had **none connected**. With no credential it silently fell back to what
needs none, which is exactly the free tier.
The fix was one call, and it persists across a serve restart (verified — a paid model still ran after
`pm2 restart officer-opencode`):
```
POST /api/integration/opencode/connect/key { "key": "<zen key>", "label": "…" } → 204
```
Afterwards `claude-sonnet-4-6` and `claude-haiku-4-5` both run on the new pipeline. **The fork is
unblocked**, and everything the table above promises — steer, queue, interrupt, resumable per-session
SSE — is available with real models.
Two consequences worth carrying:
- **alpha needs the same one-time connect** before it can use the new pipeline.
- The sidecar should do this itself at boot rather than relying on someone having run it by hand, since
a missing credential degrades to "only free models work" with no error anywhere.
### What the evidence looked like while I was getting it wrong
Recorded because the shape repeats: the failure was **silent and total** for paid models, and the cost
table drew a perfect line — every cost-0 model ran, every cost>0 model did not. I read that as a billing
boundary inside a broken pipeline. It was a billing boundary caused by an absent credential, which is a
far more ordinary explanation and one Andre reached from knowing his own setup rather than from the API.
The tell I had and did not use: the configured default is `opencode/big-pickle`, and a session created
with no model ran on `ling-3.0-tiny-free` **instead of the default**. A pipeline ignoring its configured
default is a pipeline that cannot use it — that is a credential symptom, and it was sitting in the
`/config/providers` output the whole time.
## The original diagnosis, kept for the record: only free models run
It is not sonnet, and it is not `variant`. Swept four models through `POST /api/session/{id}/model`
followed by a prompt:
| Model | New pipeline |
| -------------------- | ------------ |
| `longcat-2.0-free` | **ran** |
| `ling-3.0-tiny-free` | **ran** |
| `claude-haiku-4-5` | never ran |
| `claude-sonnet-4-6` | never ran |
| `gpt-5.1-codex-mini` | never ran |
**Every `-free` model runs; every paid model silently does not.** Ruled out along the way:
- **Not `variant`.** `claude-sonnet-4-6` advertises `["low","medium","high","max"]` and session create
echoes back `variant: "default"`, which is not among them — a promising theory that turned out to be
wrong: setting `variant: "high"` explicitly also never ran. Tested rather than assumed, which is the
whole lesson of this file.
- **Not missing credentials.** `opencode auth list` shows an OpenCode Zen API key in
`~/.local/share/opencode/auth.json` plus `ANTHROPIC_API_KEY` in the environment.
- **Not the sidecar's environment.** The *same* sidecar process runs `claude-sonnet-4-6` correctly
through `opencode run --model`, verified end to end. Same user, same home, same auth file.
So the new pipeline does not resolve paid-model credentials, and fails **silently** rather than
reporting it — while the legacy path and `run` both authenticate fine. Note it *can* surface provider
auth errors when it reaches that far: alpha's default (`nano-gpt`) returned a clean
`401 missing_api_key`. The silence is specific to opencode-zen paid models.
This reads as an upstream bug in an in-progress pipeline, not something configurable on our side.
**Consequence for the fork:** blocked, but precisely. Officer's users pick real models; a harness that
works only on free tiers is not adoptable. Re-run the sweep above after each `opencode upgrade` — the
day a paid model runs there, the migration is unblocked and worth doing immediately, because steer and
queue are already proven.
## Revised recommendation
**Take the fork, targeting the new surface, once the model question is answered.** Not the legacy
`/session/{id}/message` path — that generates fine but has neither steer nor queue, so it buys streaming
at the cost of SSE demux and warm-session lifetime for the two least interesting gaps.
Until then `opencode run --dir` stays, and it is verified working: `session:init``assistant:text`
`result` with cost, on 1.18.16, after the upgrade.
## The lesson, which is the reason this file keeps its history
This project has now hit the same trap three times, each time in a different costume:
- `directory` in the body of `POST /session` — accepted, echoed, ignored. Produced a confident wrong
answer about per-request directories.
- `location.directory` in the parity doc — a field that did not exist on that surface, which would have
compiled and silently produced `''`.
- `model` on `POST /api/session` — accepted, echoed back in the response, and fatal to execution.
**OpenCode's API accepts input it does not honour, and says nothing.** So a probe that changes one thing
and sees nothing happen has not learned that the feature is missing; it has learned that *something* is
wrong, and the next step is to remove variables, not to conclude. The corrected probes here each changed
exactly one field.
## Findings worth keeping
- `delivery` defaults to `"steer"` when omitted.
- New-surface responses wrap in `{"data": …}`; the legacy surface returns bare objects, so reading
`body.id` instead of `body.data.id` silently yields `undefined`.
- The new surface is location-scoped per request: `x-opencode-directory` header, or
`?location[directory]=` as a `deepObject` query.
- `?after=` genuinely replays a finished session's events.
- **opencode versions: both machines on 1.18.16** as of 2026-08-10 (Mac was 1.18.11, alpha 1.17.9).
Several comments recorded these backwards; corrected. Policy from Andre: **write against the latest
version regardless of what alpha happens to run.**