feat(cli): add ocx opencode launcher - #568
Merged
Merged
Conversation
opencode reads providers from a JSON config rather than env slots, so the
`ocx claude` env-injection pattern does not transfer. This adds a launcher that
generates a provider block from the proxy's visible catalog and points
OPENCODE_CONFIG at it.
The user's own opencode.json is never written to. Their effective config is read
(explicit OPENCODE_CONFIG first, then the XDG global path), merged forward into
a generated copy under the opencodex config dir, and only the `opencodex`
provider key is overwritten. Carrying the base config forward keeps the command
correct whether opencode merges the OPENCODE_CONFIG layer or replaces it, and
leaves plain `opencode` completely unchanged.
Credential handling: the generated file is written to disk and outlives the
child, so it carries opencode's documented `{env:VAR}` reference instead of the
admission key, and the real value is passed only through the child environment.
The key resolves OPENCODEX_API_AUTH_TOKEN before config.apiKeys, matching
fetchClaudeContextWindows — a non-loopback bind requires the env token and may
have no apiKeys at all, where a placeholder would 401 every request.
Model limits: limit.context is emitted only from an authoritative context
window, including native slugs via nativeOpenAiContextWindow. opencode's schema
rejects a limit block carrying context without output and CatalogModel has no
authoritative output field, so a documented budget rides along, clamped to the
context window so a small-context model is never emitted with output > context.
Robustness: opencode.json is parsed as JSONC (strict JSON first, tolerant only
on failure) because opencode documents that syntax and a commented config would
otherwise be rejected as malformed; the generated file is written atomically so
a concurrent launch cannot read a torn file; the detached proxy-start child gets
an error listener so a failed spawn reports through the health poll instead of
throwing; and a project-level opencode.json defining provider.opencodex is
detected and warned about, since opencode loads that layer last and it outranks
the generated block.
Verified against a live proxy: 76 models registered, 47 carrying authoritative
limits with no output > context violations, `opencode models opencodex` lists
all of them, and an end-to-end run through opencodex/kiro/claude-haiku-4.5
returns a completion.
3 tasks
This comment was marked as outdated.
This comment was marked as outdated.
Merge inherited OPENCODE_CONFIG_CONTENT instead of replacing it, probe the live proxy hostname for baseURL, resolve admission from env/service token/ config keys, add x-opencodex-api-key on non-loopback binds, omit native slugs in Codex Direct mode, and widen global override detection. Stabilize the Windows storage-policy concurrency test timing.
When ocx opencode spawns a detached ocx start and OPENCODEX_API_AUTH_TOKEN is absent, propagate OCX_API_TOKEN_FILE (or the default hardened path) so handleStart can load the service token before a non-loopback bind.
Use a deterministic loopback default config in provider-block helpers instead of loadConfig(), send proxy admission via apiKey on loopback and only x-opencodex-api-key on non-loopback binds, and split the docs/help examples accordingly.
Fetch the live model list from the running proxy instead of calling fetchAllModels in the CLI process, so env-backed provider keys resolve in the proxy environment and namespaced selectors carry display metadata.
Contributor
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cli/opencode.ts`:
- Around line 353-361: Bound both the fetch and response body read in the
`/api/models` request flow around `fetchImpl` and `response.text()` so a stalled
management API cannot hang indefinitely. Use the existing request-timeout
mechanism or introduce a shared deadline that covers connection, response, and
body-reading phases, and preserve the current unreachable-error handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4a0f0db8-ad00-4774-a59c-75bf3fcc7eac
📒 Files selected for processing (4)
docs-site/src/content/docs/guides/opencode.mdsrc/cli/help.tssrc/cli/opencode.tstests/opencode-cli.test.ts
Abort stalled proxy catalog requests after 8s so ocx opencode fails fast instead of hanging before OpenCode launches.
15 tasks
This was referenced Jul 30, 2026
This was referenced Aug 8, 2026
This was referenced Aug 18, 2026
agentHits
pushed a commit
to agentHits/opencodex
that referenced
this pull request
Sep 17, 2026
* feat(cli): add `ocx opencode` launcher
opencode reads providers from a JSON config rather than env slots, so the
`ocx claude` env-injection pattern does not transfer. This adds a launcher that
generates a provider block from the proxy's visible catalog and points
OPENCODE_CONFIG at it.
The user's own opencode.json is never written to. Their effective config is read
(explicit OPENCODE_CONFIG first, then the XDG global path), merged forward into
a generated copy under the opencodex config dir, and only the `opencodex`
provider key is overwritten. Carrying the base config forward keeps the command
correct whether opencode merges the OPENCODE_CONFIG layer or replaces it, and
leaves plain `opencode` completely unchanged.
Credential handling: the generated file is written to disk and outlives the
child, so it carries opencode's documented `{env:VAR}` reference instead of the
admission key, and the real value is passed only through the child environment.
The key resolves OPENCODEX_API_AUTH_TOKEN before config.apiKeys, matching
fetchClaudeContextWindows — a non-loopback bind requires the env token and may
have no apiKeys at all, where a placeholder would 401 every request.
Model limits: limit.context is emitted only from an authoritative context
window, including native slugs via nativeOpenAiContextWindow. opencode's schema
rejects a limit block carrying context without output and CatalogModel has no
authoritative output field, so a documented budget rides along, clamped to the
context window so a small-context model is never emitted with output > context.
Robustness: opencode.json is parsed as JSONC (strict JSON first, tolerant only
on failure) because opencode documents that syntax and a commented config would
otherwise be rejected as malformed; the generated file is written atomically so
a concurrent launch cannot read a torn file; the detached proxy-start child gets
an error listener so a failed spawn reports through the health poll instead of
throwing; and a project-level opencode.json defining provider.opencodex is
detected and warned about, since opencode loads that layer last and it outranks
the generated block.
Verified against a live proxy: 76 models registered, 47 carrying authoritative
limits with no output > context violations, `opencode models opencodex` lists
all of them, and an end-to-end run through opencodex/kiro/claude-haiku-4.5
returns a completion.
* fix(cli): harden ocx opencode launcher review follow-ups
Merge inherited OPENCODE_CONFIG_CONTENT instead of replacing it, probe the
live proxy hostname for baseURL, resolve admission from env/service token/
config keys, add x-opencodex-api-key on non-loopback binds, omit native slugs
in Codex Direct mode, and widen global override detection. Stabilize the
Windows storage-policy concurrency test timing.
* fix(cli): pass service token file to ocx opencode auto-start
When ocx opencode spawns a detached ocx start and OPENCODEX_API_AUTH_TOKEN
is absent, propagate OCX_API_TOKEN_FILE (or the default hardened path) so
handleStart can load the service token before a non-loopback bind.
* fix(cli): address CodeRabbit opencode review follow-ups
Use a deterministic loopback default config in provider-block helpers instead
of loadConfig(), send proxy admission via apiKey on loopback and only
x-opencodex-api-key on non-loopback binds, and split the docs/help examples
accordingly.
* fix(cli): build ocx opencode catalog from proxy /api/models
Fetch the live model list from the running proxy instead of calling fetchAllModels in the CLI process, so env-backed provider keys resolve in the proxy environment and namespaced selectors carry display metadata.
* fix(cli): bound ocx opencode /api/models fetch deadline
Abort stalled proxy catalog requests after 8s so ocx opencode fails fast instead of hanging before OpenCode launches.
---------
Co-authored-by: mihneaptu <223163739+mihneaptu@users.noreply.github.com>
3 tasks
3 tasks
lidge-jun
added a commit
that referenced
this pull request
Sep 22, 2026
…wers, and sidecar sends (#5575) * fix(web-search): combine replay-cache isolation with deadline-safe quota evidence Combines two fork PRs on the web-search path: isolate the replay cache by request context (#563) and preserve quota evidence when Retry-After would outlive the sidecar deadline (#568), rebased onto current dev. The #568 test's unrecorded-destination case is adapted to the current foldDeveloperRoleToSystem fixture semantics. bun test: web-search-bridge-replay + web-search-sidecar-429 + server-key-failover-e2e + chat-inline-document-bytes: 65 pass * fix(web-search): scope replay cells to key-resolved loopback principals * fix(retries): refuse transient 5xx after an operator-authorized reset replacement * fix(retries): word the replay refusal for the post-response path too * test(responses): keep the inline-document role fixture on its dev contract The carried replay-isolation commit also rewrote this fixture. It is unrelated to replay or retry behavior and the dev version already covers the current role contract, so it stays as dev has it. * fix(web-search): fail closed when a replay caller has no principal A loopback caller that presents no opencodex API key has no caller identity, but the replay scope gave every such caller the shared principal "loopback". Two local processes with the same conversation id and serving route then shared bridged-search replay cells, and a client-visible cell id was enough to recover another caller's retained search result. The scope now carries a principal only when resolveContextPrincipal finds one. Without it bridgeSearchReplayScope yields no scope, so nothing is recorded or restored for that caller. The key-switch regression now seeds under a real key-derived principal, with a positive control that the same seed restores and a keyless case that must leave the cell untouched. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(retries): keep every resend-inducing answer under replay suppression After an ambiguous reset spends the operator-authorized replacement, the first send may already have run the turn. The fence only covered the gateway statuses in isTransientUpstreamStatus, so a replacement that answered 429 or 529 reached the client and the proxy's quota rotation unmarked, and either could send the turn a third time. The fence now covers what actually resends: the client retry table (408, 409, 429, every 5xx) and proxy credential and quota recovery (401, 402). Those settle as the refusal with the replacement body released. Any other error keeps its real status for the caller but is marked non-replayable, so recovery loops such as the opaque-blob rebuild of a 400 cannot resend it. Successful answers are returned unchanged. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(web-search): clear a stale replay principal and document its absence Rewrite the principal field on every request so an absent principal also clears one a reused holder carried, and describe the field as absent for keyless callers instead of naming a shared loopback lane. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(retries): refuse a redirect that would resend a spent replacement A client that follows a 307 or 308 sends the same POST body again, so after a spent replacement those settle as the refusal as well. The regression tables now cover every 5xx class seen in the field and assert that a kept status is never mistaken for a proxy-synthesized refusal. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(web-search): share one physical-send budget across sidecar reset and 429 recovery Every sidecar 429 replay called fetchWithResetRetry with a fresh default allowance of three sends, so resets in front of each of the three quota legs could reach nine paid upstream requests. The sidecar now holds one three-send budget for the whole search: each helper call receives what is left and reports every send it makes, reset retries included. The budget is checked before the 429 body is released, so an exhausted budget ends with the observed 429 as the recorded outcome instead of a send-budget error recorded as a connection failure. The deadline-safe 429 handling from the carried change is unchanged. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * docs(structure): record replay ownership, the sidecar send budget, and the replacement fence Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(codex): keep a non-replayable 400 out of the gated-model account retry The answer to a spent operator replacement is marked non-replayable, and every recovery loop was meant to stop on that marker. The Codex pool's unsupported-model check did not read it, so a marked 400 from a gated model could still rebuild the turn and send it from another account. It now refuses a marked response, like the quota and transient ladders beside it. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(combos): stop on a spent replacement's answer instead of hopping The answer to a spent ambiguous-reset replacement can keep its real status with only an in-memory non-replayable marker. A combo rebuilds a failed attempt as a new response, which dropped that marker, so a context overflow or a 413 read as target-local and the combo sent the same turn to its next target although the first send may already have run it. The consumed failure now records that the attempt was non-replayable, carries the marker onto the rebuilt response, and the combo loop stops on it. A 413 also joins the refusal set, because it is answered to the client as a context overflow the client compacts and resends. A two-target combo regression counts physical sends: the second target must receive none. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * fix(errors): match the whole replay refusal sentence The refusal wording became shorter and more generic, so a provider message containing the phrase could have been labelled as this proxy's refusal in the request log. Match the full sentence the proxy writes. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * docs: state that bridged-search replay needs a caller key Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> * docs(structure): add 413 and the combo stop to the replacement fence Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
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
Maintainer takeover of #461 — lands @mihneaptu's
ocx opencodelauncher with review fixes from the maintainer pass on #461.ocx opencode [args…]: ensure proxy, buildprovider.opencodexfrom the visible catalog, launchopencodewith stdio inherited.OPENCODE_CONFIG_CONTENT); never copies or rewrites the user's global/project config, so API keys, MCP credentials, and relative{file:…}paths stay on their original files.opencode.json/opencode.jsoncto the nearest Git root and prints an informational message whenprovider.opencodexis also defined locally (runtime layer wins for this launch).guides/opencode.mdplus sidebar entry.Supersedes #461 (contributor fork could not be updated;
maintainerCanModify=false).Test plan
bun test tests/opencode-cli.test.ts— 30 passbun run typecheckbun run privacy:scanSummary by CodeRabbit
ocx opencodeto launch OpenCode via the local proxy with a generated runtime provider configuration.opencodeconfig generation/merging and routing/limits; adjusted concurrency timing assertions for policy updates.