[REPL-32382] Merge upstream opencode v1.18.32

Merged
KE
1mo ago
+281137-90574

Moves our opencode fork from upstream's July release (v1.17.14) to yesterday's (v1.18.32), every fork patch kept. The user-visible fix: choosing an effort level now works for every model. Today "Opus 5 on low/medium" and "GPT-5.6 Luna on max" silently run at the provider's default effort, because the July binary decides legal effort names from hardcoded lists that predate those models; upstream now reads them from the model catalog. Nothing here reaches the fleet until the andytown pins move.

Review in 3 steps (~15 min)

  1. Read the resolution commit. 38 files, +75/−383. This is the whole review: 29 package.json version lines and bun.lock take upstream's side (the fork never touched them), 8 source/test files are resolved by hand. The 4 that carry behavior are listed next.
  2. Skim the adaptation commit. 3 test files, +35/−19. Upstream tests compiled against interfaces the fork extends, plus two of Gabe's cost-tier tests re-pointed at models that still exist in upstream's refreshed catalog fixture.
  3. Skip the mechanical merge. 1,252 files, git's 3-way output committed verbatim with markers. Already shipped to upstream users; not ours to review.
  4. Read the REPL-32432 fix (Kevin asked for it here). 7 lines in question/resume.ts plus a test: the orphaned-reply resume clears the boot-recovery error on the turn it continues and finishes it as a normal tool-calls step, so the model sees its own question and the answer instead of asking again. Pre-existing on 1.17.14-17, not caused by the bump; found on the box during the checklist.

The 4 resolutions that carry behavior

  1. provider/transform.ts: upstream dropped OpenRouter's prompt_cache_key as "undocumented". OpenRouter documents it as the sticky routing key and build-opencode-env.mjs depends on Andrew's configurable key (#31) for OpenRouter GPT traffic. Kept: upstream's structure, our cacheKey at every site, OpenRouter restored, upstream's test flipped. Taking upstream's side would have raised OpenRouter GPT cost with no error.
  2. session/retry.ts: upstream's new retry pattern matches any message containing 502, which includes an OpenRouter overflow chunk relayed in a 200 stream. Our numeric-code branch runs first (overflow guard, then 429, then 5xx; 4xx never), upstream's text matching handles the rest.
  3. session/retry.ts: upstream caps retries at 5 globally. Ours stays per route so a fallback swap gets a fresh 5; the cap is the default when no budget is configured. Production sends maxUpstreamRetryAttempts: 3, unchanged. With no fallback, a dead provider used to spin forever; it now fails visibly after about 60s.
  4. session/compaction.ts: upstream summarizes from one serialized text message. Gabe's fallback convert hands that same message to the swapped model instead of rebuilding model messages.

What changes for us at runtime

  1. Effort levels come from the catalog; hardcoded tables are the fallback. Opus 5 efforts become adaptive thinking + effort, the same shape Sonnet 5 and Opus 4.8 already send. This is not optional: probed on the real route, Anthropic rejects thinking.type: enabled for Opus 5 ("Use thinking.type.adaptive and output_config.effort"), which is the shape 1.17.14 sends for Opus 5 high/max today, so those turns 400 on the direct route until this lands. Effort without a thinking key still thinks on Opus 5; only thinking: disabled turns it off. Kevin chose adaptive over disabled.
  2. 5-minute header and chunk timeouts by default (was none). Retries capped at 5 with jitter; DNS and network errors retry.
  3. Subagent errors surface to the parent; subagent depth is capped.
  4. Unknown config fields are ignored instead of failing startup. A typo in OPENCODE_CONFIG_CONTENT now does nothing instead of crash-looping; the checklist covers it.
  5. No new database migrations. Upstream touched zero lines of the question, MCP, server, serve, skill, or hot-reload code the fork patches.

Context

  • REPL-32382. Kevin picked this up in this Slack thread after Patrick reported "can't do opus at any effort other than high / xhigh, can't do luna at max" and Andrew asked for a standard bump checklist.
  • The checklist and the models × efforts grid: andytown #28167. Grid on fleet 1.17.14: 25 gaps. On this branch: 0.
  • Patrick's coordinator-side workaround andytown #27720 says the rebase should delete it. Hold it; close it once the rc passes the grid.
  • Previous bump, same commit shape: #16.

Testing Done

Prereleases v1.18.32-rc.1 (0921e30ec5) and rc.2 (fdceb53a7c, adds commit 4), bun 1.3.14, installed through the tag's own installer on a real production box (Kevin's project 98f99592…, sandbox 5885918f…, fleet version 1.17.14-17 before and after). The checklist from andytown #28167, all ten rows:

  1. Models × efforts: offline grid 33 models, 0 unexpected gaps, 0 stale. Live on the box through the real proxied providers, 20/20 pairs accepted with the variant recorded on the assistant message: Opus 5 low/medium/xhigh, Luna/Terra/Sol max, Sonnet 5 max (direct and via OpenRouter), Haiku 4.5 high, Gemini 3.8 Flash low, GLM-5.3 low/max, Kimi K3 low/max, DeepSeek V4 Flash max, V4 Pro xhigh, V4.1 Flash max, Muse Spark max, Qwen 3.8 Max xhigh, Grok 4.6 xhigh.
  2. Skills: skill:memory loaded and read in a turn.
  3. Tools: bash (native), get-branding (harness), execute → tools.bedrock.get_workspace (code mode, MCP) in one turn.
  4. Plugins: all six harness plugins loaded at boot; the turn in row 3 ran through them.
  5. Questions: ask → answer via the coordinator RPC → "Red". Ask → restart opencode → the question row survived in question_request, the answer was routed to the orphaned request (reply for orphaned request), and the turn resumed. On rc.1 the model then re-asked the question (the REPL-32432 finding below); on rc.2, with the fix in commit 4, the same flow replied "Blue" with no second asking id= line and question_request drained to 0.
  6. Hot reload: s6-svc -h kept pid 3940 and logged harness reload { envApplied: 65 } and mcp headers refreshed.
  7. Fallback (Gabe's #42 and follow-ups), exercised live on rc.2 in a second opencode instance on the same box (own data dir, production's exact fallback policy plus a stub provider that always returns 503). Broken route mapped to real OpenRouter Sonnet 5: the turn hit the stub 4 times (1 + maxUpstreamRetryAttempts 3), logged [model-fallback] switched model after provider failure from=broken/sonnet to=openrouter/anthropic/claude-sonnet-5, and was answered by the fallback with the assistant message stamped openrouter/anthropic/claude-sonnet-5; the next turn on the same requested route made 0 stub hits and answered in 1.7s (cooldown routing). Two broken routes mapped to each other: 12 hits (3 routes × 4), exactly 2 swaps (maxFallbackAttempts), then the turn failed visibly with the provider's error after 52s, stamped with the requested model. No loop.
  8. Restart mid-turn: SIGKILL during a sleep 120 bash call; boot logged recovered interrupted turn, the coordinator shows the session as error with a failed prompt timeline, nothing stuck busy.
  9. Overflow: three ~84k-token pastes on OpenRouter Sonnet 5; one compaction turn ran between pastes 2 and 3 and every turn finished stop; the small turn after it read 152k cached tokens. The overflow was caught proactively, so the provider-relayed 502 path was not hit live; its unit test is green.
  10. Cost and downgrade: same session, OpenRouter Sonnet 5. Old binary warm turn: 67,195 cached tokens read, $0.0137. First rc turn: cold (the prompt prefix grew by 107 tokens with upstream's prompt edits). Second rc turn: 67,302 read (100%), $0.0136. Then 1.17.14-17 reinstalled on the same database: boots, lists all 27 sessions including the rc's, completes a warm turn. Box left on 1.17.14-17.

Also green before the box: fork CI on this branch; locally bun typecheck, opencode 3655/0, HttpApi exerciser 208/208, core migration gate 18/18.

One finding, pre-existing, fixed in commit 4: a question answered after an opencode restart got asked again, because recover.ts stamps the orphaned turn with an Unknown error and toModelMessages then drops that turn (and the healed answer) from model history. Reproduced identically on 1.17.14-17 during the downgrade drill (REPL-32432). The new test runs the real SessionRecovery before the reply and asserts the fake model's request carries the answer as its only tool message; it fails without the fix. v1.18.32-rc.2 carries the fix and was verified live on the same box as above.

Rollback: revert the pin PR; boxes converge back to 1.17.14-17. No new migrations, so downgrade is schema-safe.

🤖 Generated with Claude Code