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>
156 lines
9.1 KiB
Markdown
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.**
|