feat: add opencode-free as a zero-credential default provider - #1720
Merged
Conversation
Maka must be usable out of the box with zero credentials. opencode-free is a first-class provider that exposes OpenCode Zen's free, anonymous, IP-limited models over https://opencode.ai/zen/v1 with no API key (the server treats the omitted Authorization as anonymous, matching the upstream OpenCode client behavior for its free tier). It is distinct from opencode (Zen, pay-as-you-go) and opencode-go (Go subscription): both of those stay api_key, because their models are paid. opencode-free carries only the active, tool-capable, cost=0 free models from the models.dev opencode snapshot (big-pickle, mimo-v2.5-free, nemotron-3-ultra-free, deepseek-v4-flash-free, north-mini-code-free, hy3-free), pinned explicitly because the snapshot has no cost field. Model metadata is reused via modelsDevId 'opencode' (mirroring the xai-oauth -> xai pattern), so no model entries are duplicated. It leads the recommended providers (recommendedOrder 0) and leads the ready/catalog orders so a fresh Maka install surfaces free models first.
A fresh Maka install now seeds an opencode-free connection with no secret and defaults to it, so the app is usable out of the box before any provider key is configured. Env-keyed providers (ANTHROPIC_API_KEY / OPENAI_API_KEY) still layer on top and take the default when present, preserving the prior env bootstrap precedence (Anthropic before OpenAI). The seed decision is extracted into resolveBootstrapConnections (pure & sync, in @maka/core) so it is testable without Electron; app-lifecycle's ensureBootstrapConnection now drives the connectionStore from that decision. opencode-free's authKind 'none' bypasses the secret readiness gate (connection-readiness), so the seeded connection is ready immediately. provider-display-copy gains the opencode-free catalog entry (badge 'Free'), and icon-governance's OpenCode mark contract is widened from 'both plans' to 'all OpenCode plans' to cover the third, free tier.
Add a runtime-level fake-fetch test that directly pins what the opencode-free free tier depends on: an empty apiKey produces a request with NO Authorization header, posted to zen/v1/chat/completions with the right model id. This invariant was previously only inferred from 'the code path looks like Ollama's'; now it is owned by opencode-free itself, so a future AI SDK change in empty-key handling or a misguided placeholder-key injection fails here before the anonymous free path silently breaks. Also fix the opencode-free provider test comment: the runtime passes an empty key (and @ai-sdk/openai-compatible omits Authorization), not a 'placeholder'.
Astro-Han
force-pushed
the
feat/opencode-free-default-models
branch
from
July 31, 2026 17:48
89d9b8c to
6013c08
Compare
This was referenced Jul 31, 2026
jackwener
added a commit
that referenced
this pull request
Jul 31, 2026
Second pass of the test cleanup, now repo-wide rather than desktop-only. A scan of all 811 test files sorted them by whether they execute the code they assert about; these do not: - 6 scripts/cu-*-contract tests regexed OTHER SCRIPTS' source for string literals (`/claude-sonnet-4-6/`, `/parallel_tool_calls: false/`). They ran nothing and broke on any legitimate edit of the file they watched. - 3 runtime contracts pinned import structure and retired identifier names by scanning the monorepo source tree — lint rules wearing a test costume. - 3 prN-localized-copy-contract files scanned a hardcoded list of ~40 renderer files for inline CJK. They were scaffolding for an i18n migration that finished; their shared helper went with them. The bilingual invariant they nominally guarded is still covered by the copy-catalog tests that call the catalogs (shell-copy, settings-preferences-copy, and friends), which assert no silent Chinese fallback in English. - 1 type-only test whose body was three `assertType<...>()` calls: zero runtime assertions, so it could not fail at runtime. tsc owns that. Also in this pass, e2e: - first-run.spec.ts asserted an onboarding registry screen that #1720 removed from the default flow (zero-credential `opencode-free` resolves a fresh install to `ready_empty`). Its 4 tests failed on main. Reduced to the boot smoke its own docblock called the valuable part; the provider catalog it browsed is covered by providers.spec.ts. - The suite ran `workers: 1` on the theory that parallel Electron windows fight over screen/IPC. They don't: the fixture mkdtemps a userData dir per test. Measured on the full suite: 1 worker ≈ 7min, 4 ≈ 2.4min. CI goes to 2 for headroom on a shared runner, local to 4. Kept, deliberately: static guards that protect security or packaging boundaries (preload/IPC surface, renderer token leak, noopener), the compiler-API dependency-boundary tests, and the rendered-geometry e2e specs — sidebar-geometry and scroll-geometry lock real regressions (the footer pushed off-screen at 60 sessions; the endless-scroll content-visibility bug) that no unit test can reach.
Astro-Han
added a commit
that referenced
this pull request
Jul 31, 2026
#1720 (opencode-free as a zero-credential default provider) added `ensureBootstrapConnection`, which unconditionally seeds an `opencode-free` connection on first launch when no connections exist. The E2E gate that skips it, `if (!e2eFixture)`, only triggers when `MAKA_E2E_FIXTURE` is set, but the `emptyWindow` fixture (used by all four `first-run.spec.ts` tests) passes `seed: false` and no `e2eFixtureScenario`, so `MAKA_E2E_FIXTURE` is unset and the bootstrap runs — seeding `opencode-free` into the "empty" workspace. The onboarding state becomes `ready_empty` instead of `needs_connection`, so `OnboardingHero` no longer renders `.maka-firstrun-row`, and the four tests fail with `toHaveCount 0` / click timeouts. This is a deterministic regression on current main (not a flake): #1720's own e2e failed exactly these four; every post-#1720 run fails them identically; pre-#1720 runs passed. #1720 merged with the failing e2e, so every PR's e2e is now red on these four. The `needs_connection` first-run hero is now unreachable for a fresh install (opencode-free is always seeded, and re-seeded whenever the connection list is empty), so these tests assert a contract that no longer holds. Drop them now to unblock e2e; follow-up will add coverage for the post-#1720 fresh-install boot contract (opencode-free seeded -> ready_empty). Also remove the now-unused `emptyWindow` fixture (it was "Used by first-run only"). Unblocks the plan-reminder focus-restore fix (#1723) and the docs PR #1719, whose e2e runs merge with main and so also hit this cluster. Verified: `npx playwright test --list` parses the remaining 93 tests across 32 files with no import/syntax errors; `emptyWindow` has no remaining references. The 93 remaining tests passed in the CI runs that failed only on these four.
jackwener
added a commit
that referenced
this pull request
Aug 1, 2026
Second pass of the test cleanup, now repo-wide rather than desktop-only. A scan of all 811 test files sorted them by whether they execute the code they assert about; these do not: - 6 scripts/cu-*-contract tests regexed OTHER SCRIPTS' source for string literals (`/claude-sonnet-4-6/`, `/parallel_tool_calls: false/`). They ran nothing and broke on any legitimate edit of the file they watched. - 3 runtime contracts pinned import structure and retired identifier names by scanning the monorepo source tree — lint rules wearing a test costume. - 3 prN-localized-copy-contract files scanned a hardcoded list of ~40 renderer files for inline CJK. They were scaffolding for an i18n migration that finished; their shared helper went with them. The bilingual invariant they nominally guarded is still covered by the copy-catalog tests that call the catalogs (shell-copy, settings-preferences-copy, and friends), which assert no silent Chinese fallback in English. - 1 type-only test whose body was three `assertType<...>()` calls: zero runtime assertions, so it could not fail at runtime. tsc owns that. Also in this pass, e2e: - first-run.spec.ts asserted an onboarding registry screen that #1720 removed from the default flow (zero-credential `opencode-free` resolves a fresh install to `ready_empty`). Its 4 tests failed on main. Reduced to the boot smoke its own docblock called the valuable part; the provider catalog it browsed is covered by providers.spec.ts. - The suite ran `workers: 1` on the theory that parallel Electron windows fight over screen/IPC. They don't: the fixture mkdtemps a userData dir per test. Measured on the full suite: 1 worker ≈ 7min, 4 ≈ 2.4min. CI goes to 2 for headroom on a shared runner, local to 4. Kept, deliberately: static guards that protect security or packaging boundaries (preload/IPC surface, renderer token leak, noopener), the compiler-API dependency-boundary tests, and the rendered-geometry e2e specs — sidebar-geometry and scroll-geometry lock real regressions (the footer pushed off-screen at 60 sessions; the endless-scroll content-visibility bug) that no unit test can reach.
jackwener
added a commit
that referenced
this pull request
Aug 1, 2026
* chore: drop source-regex, migration, and type-only tests Second pass of the test cleanup, now repo-wide rather than desktop-only. A scan of all 811 test files sorted them by whether they execute the code they assert about; these do not: - 6 scripts/cu-*-contract tests regexed OTHER SCRIPTS' source for string literals (`/claude-sonnet-4-6/`, `/parallel_tool_calls: false/`). They ran nothing and broke on any legitimate edit of the file they watched. - 3 runtime contracts pinned import structure and retired identifier names by scanning the monorepo source tree — lint rules wearing a test costume. - 3 prN-localized-copy-contract files scanned a hardcoded list of ~40 renderer files for inline CJK. They were scaffolding for an i18n migration that finished; their shared helper went with them. The bilingual invariant they nominally guarded is still covered by the copy-catalog tests that call the catalogs (shell-copy, settings-preferences-copy, and friends), which assert no silent Chinese fallback in English. - 1 type-only test whose body was three `assertType<...>()` calls: zero runtime assertions, so it could not fail at runtime. tsc owns that. Also in this pass, e2e: - first-run.spec.ts asserted an onboarding registry screen that #1720 removed from the default flow (zero-credential `opencode-free` resolves a fresh install to `ready_empty`). Its 4 tests failed on main. Reduced to the boot smoke its own docblock called the valuable part; the provider catalog it browsed is covered by providers.spec.ts. - The suite ran `workers: 1` on the theory that parallel Electron windows fight over screen/IPC. They don't: the fixture mkdtemps a userData dir per test. Measured on the full suite: 1 worker ≈ 7min, 4 ≈ 2.4min. CI goes to 2 for headroom on a shared runner, local to 4. Kept, deliberately: static guards that protect security or packaging boundaries (preload/IPC surface, renderer token leak, noopener), the compiler-API dependency-boundary tests, and the rendered-geometry e2e specs — sidebar-geometry and scroll-geometry lock real regressions (the footer pushed off-screen at 60 sessions; the endless-scroll content-visibility bug) that no unit test can reach. * chore(desktop): drop the remaining source-pinning tests The first sweep matched `readFile(`, so nine files using `readFileSync` survived it. They are the same species — assertions about the text of the project's own source: - storybook-baseline-contract (21 tests) pinned story structure - onboarding-one-time-regression asserted that a source expression still reads `sessions.length === 0` - main-process-wiring-contract asserted main.ts imports each registrar - build-hygiene-contract asserted package.json still declares certain npm scripts - chip-converge, reactive-locale-foundation, form-consumer-ownership, startup-loading-shell, opencli (a version pin the lockfile already owns) @babel/parser was only there to parse source for one of them, so it leaves with them; knip is back to zero findings for apps/desktop. headless-storage-dependency stays: it uses the TypeScript compiler API to walk the real dependency graph rather than regexing text. * chore: delete contracts that assert the shape of source code A third pass, this time reading the tests rather than pattern-matching them. What turned up is the same species wearing a disguise: files that import a real module (so the earlier scans cleared them) but whose assertions are regexes over the project's own .tsx/.ts/.css text. Deleted outright: - icon-governance-contract (1029 lines) — banned inline <svg> and direct lucide imports by scanning source. A lint rule. - settings-roadmap-cleanup-contract — asserted that deleted code stayed deleted ("must not keep the generic ComingSoon template"), including extracting a function's source text to assert on its body. - composer-mention-contract — matched the onTextareaKeyDown body out of composer.tsx and compared indexOf positions to pin the ORDER of its if branches. The behavior is covered end-to-end by composer-skill-invocation.spec.ts. - settings-app-info / settings-network / settings-theme / daily-review-copy-feedback / voice-capture-smoke — all assert React source text ("catch(...) { setInfoError(message) }") rather than rendering anything. - custom-relay-provider-contract, and three extraction contracts in core and headless that pinned a refactor's file split in place ("the schema lives in a leaf with no controller dependency"). One of them regexed a harbor script knip already reports as unused. Trimmed rather than deleted: session-project-view-contract kept its 11 rendering tests (project grouping, aliases, filters, lifecycle rows) and lost the 5 that regexed sidebar.css and app-shell.tsx useMemo bodies. Kept, again deliberately: every guard whose subject is a security or credential boundary — the IPC surface pins, the renderer token-leak gate, cleartext-key echo, the subscription experimental flag re-check in main. Those assert source text too, but what they protect is not taste. * fix(e2e): keep CI serial until the focus-sensitive specs are hardened A 2-worker CI run failed exactly where parallel Electron windows share a resource that per-test userData dirs do not isolate: OS focus. composer-mode-indicator compares hover backgrounds and plan-reminders asserts toBeFocused(); Chromium blurs the document when another window steals activation, so both fail intermittently. CI goes back to 1 worker — green, and identical to today's behavior. Local keeps 4, which is where the dev loop feels the 7min → 2.4min difference. Hardening those two specs (bring the window to front before asserting hover/focus) is what unlocks parallel CI. * chore(desktop): drop the bot runtime consistency source contract Two tests, both regexes over the Settings source asserting that the bot provider tables stay wired the same way. The tables themselves are typed and exported from @maka/core; a divergence is a type error, not a missing string match. * chore(scripts): collapse the per-provider model-metadata permutations sync-model-metadata.test.mjs carried 25 provider tests with one shape: mkdtemp, build a catalog holding one provider, spawn a node process to run the sync script, assert the generated file contains that provider's id and one model's displayName. They exercise the same generic mapping path 25 times with different literals, at one process spawn each. Kept two as representatives — Cerebras for the plain provider shape, ZenMux for the creator/model id shape — plus every test that exercises a real branch: provider separation (Z.AI direct vs coding plan, GitHub Copilot vs GitHub Models), the multi-plan families (Tencent ×3, StepFun ×3, Xiaomi's three Token Plan regions), OpenCode protocol overrides, modality mapping, and the three rejection paths. 30 tests → 16, 1324 lines → 766, and 14 fewer process spawns per run. * chore: cut prompt-prose pins, a barrel tautology, and source-regex tails Reading the small files turned up two species the scans had no signal for. Barrel tautology — sandbox-export.test.ts imported the same seven symbols through `../index.js` and `../sandbox/index.js` and asserted they were `===`. The module system guarantees that, and a dropped re-export is a compile error in this very file, so the test could never fail on its own. Prompt-prose pins — swarm-mode.test.ts and graph-mode.test.ts each render a prompt and assert it contains ten English phrases ("preferred default execution strategy", "Do not manufacture parallelism"). They assert nothing about behavior and break on any rewording, which is what prompts get. Deleted both. Prompt tests that check interpolation stay, since those verify the values a prompt carries rather than its prose — session-environment-prompt asserting `Git branch: main` is a real test. Source-regex tails, trimmed rather than deleted: - project-context-badge kept the two tests that resolve real git metadata in tmpdirs (normal checkout, worktree gitdir, upward root walk) and lost the seven that regexed main.ts for exact expressions like `fallbackRoots: () => [process.cwd(), app.getAppPath()]`. 273 lines → 54. - attachment-frontend-contract had one test that renders a ChatView with an image attachment; the other seven regexed source and CSS tokens. Reduced to that one and renamed to attachment-chat-render, since it no longer pins anything. * fix(test): preserve behavioral coverage after pruning
Astro-Han
added a commit
that referenced
this pull request
Aug 20, 2026
Regenerate the checked-in catalog from a single fetch of https://models.dev/api.json on 2026-08-20, sha256 da57b8e4281fd65d725ef7f2f73f681d052848c07737c4aaa1c707d91ad26e33. The digest records which payload produced this snapshot. models.dev serves a rolling document and the payload is not archived here, so this is a provenance record, not a reproducibility guarantee. Models 1690 -> 1779, deprecated 82 -> 98. Pricing gains 86 entries, drops 9, and reprices 37 — openrouter:~google/gemini-flash-latest input falls 1.5 -> 0.375, deepinfra:moonshotai/Kimi-K3 rises 2.7 -> 2.85. Two consumers had to follow, and nothing more. Upstream deprecated north-mini-code-free and laguna-s-2.1-free, which provider-registry validates at module load, so importing the registry threw. Removing the two ids is the response that validation exists to force. The four that remain are unchanged from main and are still checked for active and tool-capable on every import. They are the ids #1720 chose by transcribing that same rule out of the snapshot — "active, tool-capable, cost=0", pinned by hand only because the snapshot carries no cost field. Nothing here establishes that an anonymous caller can actually reach them, and nothing here changes which models a user can reach: this refresh only drops two the upstream retired. gemini-3-pro-preview left the catalog, so the Google thinking test moves to gemini-3.1-pro-preview, whose effort values carry the same no-off shape the test asserts. Generated-by: Claude Code
6 tasks
Astro-Han
added a commit
that referenced
this pull request
Aug 20, 2026
Regenerate the checked-in catalog from a single fetch of https://models.dev/api.json on 2026-08-20, sha256 da57b8e4281fd65d725ef7f2f73f681d052848c07737c4aaa1c707d91ad26e33. The digest records which payload produced this snapshot. models.dev serves a rolling document and the payload is not archived here, so this is a provenance record, not a reproducibility guarantee. Models 1690 -> 1779, deprecated 82 -> 98. Pricing gains 86 entries, drops 9, and reprices 37 — openrouter:~google/gemini-flash-latest input falls 1.5 -> 0.375, deepinfra:moonshotai/Kimi-K3 rises 2.7 -> 2.85. Three consumers had to follow, and nothing more. Upstream deprecated north-mini-code-free and laguna-s-2.1-free, which provider-registry validates at module load, so importing the registry threw. Removing the two ids is the response that validation exists to force. The four that remain are unchanged from main and are still checked for active and tool-capable on every import. They come from the set #1720 chose by transcribing "active, tool-capable, cost=0" out of the snapshot, pinned by hand only because the snapshot carries no cost field. Individual ids have had operational attention since — #1720 called big-pickle anonymously, #2166 replaced an unhealthy bootstrap model and probed the fallbacks — but the four have never been verified as a set, and this refresh adds none. It also adds no reachable model: it only drops two the upstream retired. gemini-3-pro-preview left the catalog, so the Google thinking test moves to gemini-3.1-pro-preview, whose effort values carry the same no-off shape the test asserts. The snapshot now sits about 2 KB under Biome's 1 MiB default, and this refresh alone added 57 KB, so the next one crosses it. An over-size file is skipped with a warning while format:check still exits 0, so crossing would quietly stop checking the snapshot rather than fail on it. The ceiling is raised for that one file through an override; a global raise would also disarm the guard for hand-written files, which is what it exists for. Generated-by: Claude Code
Astro-Han
added a commit
that referenced
this pull request
Aug 20, 2026
The committed models.dev snapshot had drifted far enough that it no longer described the catalog it claims to mirror: 1690 models to 1779, deprecations 82 to 98, and pricing rows 1239 to 1316 (+86 new, -9 removed, 37 repriced). The refresh is regenerated from one pinned api.json payload rather than a live fetch, so every number here is reproducible from a fixed input. Three consumers had to follow. - Upstream deprecated two of the four ids pinned for the OpenCode Free offering, `north-mini-code-free` and `laguna-s-2.1-free`. Both are removed. `provider-registry.ts` asserts its pins are active and tool-capable at module load, so leaving them would fail the process at import, not at first use. - `gemini-3-pro-preview` left the catalog, so the Google thinking test moves to `gemini-3.1-pro-preview`, whose effort values carry the same no-`off` shape the test asserts. Two Google fallback ids the snapshot no longer contains, `gemini-2.0-flash` and `gemini-1.5-pro`, are dropped from that list. - The snapshot now sits about 2 KB under Biome's 1 MiB default, and this refresh alone added 57 KB, so the next one crosses it. An over-size file is skipped with a warning while `format:check` still exits 0, so crossing would quietly stop checking the snapshot rather than fail on it. The ceiling is raised for that one file through an override; a global raise would also disarm the guard for hand-written files, which is what it exists for. The refresh also flips 15 ZenMux models from active to deprecated. Two carry a model-level protocol override and are excluded from the fallback source, so 13 reach it. Discovery keeps only the fallback set and the catalog marks what survives available and default-capable, so all 13 would have been offered as usable choices for a provider that had none before. ZenMux now filters deprecated ids at the call site, as six other providers already do. That fix is deliberately narrow. Six providers still ship deprecated ids in their fallback lists — 28 models across `togetherai`, `mistral`, `xiaomi`, `deepinfra`, `nvidia`, and `openai`, the last writing its list by hand rather than through `toolCallingModelIds`. Reverting this refresh changes none of them, so converging them is tracked in #3355, and a new contract test records that set as an explicit boundary which fails both on regression into it and on a listed provider being cleaned up without being removed. Not addressed here: persisted user catalogs keep ids this refresh retires, because nothing reconciles `enabledModelIds` against the current registry and `opencode-free` has no discovery contract that could later repair it. That predates this branch — #2050 removed `hy3-free` the same way and touched no persistence — and reverting this refresh does not fix it, since the ids stay retired upstream. Tracked in #3354. The four surviving OpenCode Free pins are not evidence of anonymous entitlement and are not claimed as such. Individual ids have had operational attention — #1720 called `big-pickle` anonymously, #2166 replaced an unhealthy bootstrap model — but the four have never been verified as a set, and this refresh adds no verification and no reachable model; it only drops two the upstream retired. Reviewed by @hqhq1025 with Codex assistance under the maintainer-approved review workflow; two findings were raised, one fixed here and one filed as #3354. Generated-by: Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Maka now works out of the box with zero credentials. This adds
opencode-freeas a first-class provider exposing OpenCode Zen's free, anonymous, IP-limited models overhttps://opencode.ai/zen/v1with no API key, and seeds it as the default connection on first launch.The free tier is not an OAuth account login. The mechanism is: the client omits
Authorizationentirely, the OpenCode server treats the request as anonymous, and models flaggedallowAnonymousare served subject to per-IP rate limiting (FreeUsageLimitErroron overflow). This matches the upstream OpenCode client's free-tier behavior.opencode-go(Go subscription) staysapi_key— its models are allcost > 0.opencode-freeprovider (authKind: 'none',recommendedOrder: 0): 6 active, tool-capable,cost=0models pinned from the models.dev opencode snapshot (big-pickle,mimo-v2.5-free,nemotron-3-ultra-free,deepseek-v4-flash-free,north-mini-code-free,hy3-free). Model metadata is reused viamodelsDevId: 'opencode'(mirroringxai-oauth -> xai), so no model entries are duplicated.resolveBootstrapConnections(pure, sync, in@maka/core, testable without Electron) decides the first-launch seed:opencode-freeis always seeded; env-keyed providers (ANTHROPIC_API_KEY/OPENAI_API_KEY) layer on top and take the default when present, preserving the prior Anthropic-before-OpenAI precedence.app-lifecycle.ensureBootstrapConnectiondrives the connectionStore from this decision.opencode-freeauthKind: 'none'bypasses the secret readiness gate, so the seeded connection is ready immediately. Runtime uses the existingopenai-compatiblepath (same as Ollama) with an empty apiKey, so no runtime changes are needed.Verification
@maka/core: 1223 tests pass (npx tsx --test), build + tsc clean.apps/desktop:tsc -p tsconfig.main/renderer/preloadclean; icon-governance 35, provider-display-copy-contract 2, and 117 provider/onboarding contract tests pass.npm run format:check(1339 files),npm run lint(2421 files), andnpm run typecheck(all workspaces) clean.main(80792ec8c, refactor(ui): adopt Astryx disclosures #1711) with no conflicts.https://opencode.ai/zen/v1: both "no Authorization" andAuthorization: Bearer publicreturn200forGET /v1/modelsandPOST /v1/chat/completions(modelbig-pickle), withcost:"0"— confirming the empty-apiKey runtime path is anonymously admitted.Not done (intentionally deferred):
FreeUsageLimitError/GoUsageLimitErrorupgrade-guidance UI.opencode-free(it shares the Ollamaopenai-compatiblepath, which is already covered).