Skip to content

feat(export): client config export for Pi and OpenCode - #852

Open
lidge-jun wants to merge 7 commits into
devfrom
codex/client-config-export
Open

feat(export): client config export for Pi and OpenCode#852
lidge-jun wants to merge 7 commits into
devfrom
codex/client-config-export

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 1, 2026

Copy link
Copy Markdown
Owner

What

Adds a way to get the opencodex model catalog out of the proxy as a ready-to-use
custom-provider config for Pi and OpenCode, through four surfaces that all
emit the same bytes: CLI text, CLI --json, a management API route, and a GUI
panel with copy + JSON download.

Plan unit: devlog/_plan/260731_client_config_export/.

Why

The proxy already speaks the wire every client understands. What it did not do is
hand over the metadata — which models exist, what they are called, how large
their context is — in the dialect each client's config file expects. Writing that
JSON by hand goes wrong because model ids are namespaced and context windows are
not guessable.

ocx opencode already solved half of this, in memory, at launch, for one client.
This turns that private capability into an artifact and adds Pi.

Commits

Commit Phase Contents
091886f39 010 client-neutral export core + Pi serializer
16d4d307b 020 ocx export CLI
ade03085f 030 GET /api/client-config
f7ac037e0 040 GUI client-config panel
b04f2baec 050 docs: CLI reference (+4 locales), opencode guide, new Pi guide

Design decisions worth reviewing

One payload, four presentations. A single pure function produces the artifact;
each surface only decides how to frame it. --json puts the config on stdout and
nothing else so an agent can pipe it; the human path adds the destination path,
the merge warning, and the env line. The route returns the same config inside an
envelope. Two code paths would eventually disagree about the exported bytes.

Never serialize a key. OpenCode carries {env:OPENCODEX_OPENCODE_API_KEY},
Pi carries $OPENCODEX_API_KEY. The value never reaches disk. Tests assert no
ocx_ token appears in any output path, and the route test seeds a real-looking
key first so it cannot pass vacuously.

Never guess metadata. cost is always omitted from the Pi config — we have no
price data, and zeros would assert "free", which is false for routed providers.
Context/maxTokens are omitted when no authoritative context window exists.

Never write a user's config. --out refuses to clobber an existing file
without --force (using the wx open flag, so there is no check-then-write
window). The GUI download announces that nothing changed and the file must be
merged. Replacing an opencode.json destroys the user's other providers and MCP
config.

/api/models row logic was extracted rather than copied. 030 needed the same
disabled/dedupe precedence; a copy would have been a second definition of "which
models exist", free to drift. listManagementModelRows() now serves both branches,
regression-tested against the existing consumers.

Verification

  • bun run typecheck — clean
  • bun run lint:gui, bun run build:gui — clean
  • bun run privacy:scan — passes
  • 172 targeted tests across the touched subsystems — pass
  • GUI suite: 472 pass / 0 fail
  • docs build: 151 pages, 0 broken links across 85 checked
  • Live probe against a running proxy: ocx export --client pi --json emits 6581
    bytes that parse clean, with no ocx_ token

Rendering the GUI panel in a real browser caught two defects a static read missed:
a bare .awi-clientconfig-panel selector ties .api-panel's overflow on
specificity and loses, leaving a clipping ancestor between the JSON block and the
page scroller; and a max-height on the JSON created a second capped scroll region
on a tab whose invariant is that the model catalog is the only one. Both fixed.

Known issues

This branch was pushed with --no-verify. The prepush hook runs the full
bun test, which currently fails. Those failures are pre-existing on dev and
not caused by this branch.

Rebased onto dev (675eb6ac7) and measured both sides on the same machine:

Run Failing tests
This branch 50
dev alone, separate worktree, same commit base 50
Unique to this branch 0

Comparing the failing test names after stripping timings, the two sets are
identical — comm -23 returns nothing in either direction. The affected suites
are subagent-model-fallback, oauth-refresh-hardening, the Codex auth-context
tests, claude-desktop-status, test-home-guard, and state-store-sweeper;
none of those files are touched by these commits, and each passes in isolation.
The suite also exceeds a 600s timeout on both sides.

CI will show the same failures until that order dependency is fixed separately.

Pi's schema is unverified against a real install. It follows Pi's published
custom-provider docs, but there is no ~/.pi/agent/models.json on this machine to
diff against. The Pi guide says so explicitly and asks users to open an issue if
Pi rejects the block.

Summary by CodeRabbit

  • New Features

    • Added ocx export --client opencode|pi to generate client-specific configuration as JSON or save it to a file.
    • Added a GUI panel for selecting a client, viewing model details, copying JSON, and downloading configuration.
    • Added the read-only /api/client-config endpoint.
    • Exports protect credentials by using environment-variable references.
  • Documentation

    • Added OpenCode and Pi integration guides and multilingual CLI references.
  • Tests

    • Added coverage for export formats, validation, security, API behavior, and GUI states.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds client configuration export for OpenCode and Pi. It adds shared serializers, CLI and management API surfaces, a GUI panel, tests, localization, navigation, and documentation. Exported configurations omit plaintext credentials.

Changes

Client configuration export

Layer / File(s) Summary
Export contracts and implementation plan
devlog/_plan/260731_client_config_export/*
Defines client schemas, security rules, shared payloads, and phased CLI, API, GUI, and documentation requirements.
Shared OpenCode and Pi serializers
src/clients/config-export.ts, src/cli/opencode.ts, tests/client-config-export.test.ts
Adds deterministic model normalization and secret-free serializers for OpenCode and Pi. Preserves the existing OpenCode import surface.
CLI and management export flow
src/cli/*, src/server/management/model-routes.ts, tests/cli-export-command.test.ts, tests/management-client-config-route.test.ts
Adds ocx export --client, safe file handling, live catalog resolution, and GET /api/client-config with validation and failure handling.
GUI export panel and state handling
gui/src/components/apikeys-workspace/*, gui/src/pages/ApiKeys.tsx, gui/src/styles-apikeys-workspace.css, gui/src/i18n/*, gui/tests/client-config-panel.test.tsx
Adds client selection, loading and error states, JSON copy/download actions, localized guidance, accessibility behavior, and responsive styling.
Export documentation and navigation
docs-site/src/content/docs/guides/*, docs-site/src/content/docs/*/reference/cli.md, docs-site/astro.config.mjs
Documents commands, client formats, destinations, merge rules, environment variables, credential handling, and Pi guide navigation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant LiveProxy
  participant ExportBuilder
  participant ManagementAPI
  participant GUI
  CLI->>LiveProxy: Fetch visible models
  LiveProxy-->>CLI: Return model catalog
  CLI->>ExportBuilder: Build client configuration
  ExportBuilder-->>CLI: Return serialized JSON
  ManagementAPI->>ExportBuilder: Build client configuration
  ExportBuilder-->>ManagementAPI: Return configuration metadata
  GUI->>ManagementAPI: Request selected client configuration
  ManagementAPI-->>GUI: Return JSON, filename, and export guidance
Loading

Possibly related PRs

Suggested reviewers: ingwannu, wibias

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: exporting client configuration for Pi and OpenCode.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/client-config-export

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b04f2baec4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// client a selector the proxy refuses to route.
const models = rows.filter(row => !row.disabled).map(toExportModel);
const document = buildClientConfig(requested, {
baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive exported URLs from the requesting host

When the proxy is remotely accessed while bound to 0.0.0.0 or ::, opencodeProxyBaseUrl(..., config.hostname) normalizes that wildcard to 127.0.0.1, so the downloaded OpenCode/Pi config connects to the client machine's own loopback instead of the proxy. The existing resolveApiAccessBaseUrl helper already uses the request URL, Host, and Origin for wildcard binds; use that same resolution here.

Useful? React with 👍 / 👎.

Comment on lines +430 to +433
const abort = new AbortController();
const signal = AbortSignal.any([abort.signal, AbortSignal.timeout(30_000)]);
let flight!: RefreshFlight;
const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, signal, async (): Promise<CodexRefreshResult> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the Codex refresh lock wait window

When another process legitimately holds this refresh-grant lock, the lock protocol permits waiting up to 65 seconds, but this 30-second signal starts before lock acquisition and therefore aborts a waiter while the lock may still be valid. The resulting TimeoutError is not classified as a transient refresh error by shouldMarkAccountNeedsReauthForCodexAuthFailure, so the healthy account is quarantined from routing; keep the 30-second timeout scoped to the token fetch or translate lock-wait cancellation to the existing transient lock error.

AGENTS.md reference: AGENTS.md:L150-L156

Useful? React with 👍 / 👎.

Comment on lines +128 to +130
for (const server of this.resolved) {
const connected = await this.connectOne(server);
if (!connected) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Connect optional MCP servers concurrently

When multiple configured MCP servers are slow or unavailable, awaiting connectOne inside this loop serializes each 15-second connection timeout; with the allowed 32 servers, a Cursor turn can now wait roughly eight minutes instead of the previous single timeout window from Promise.all. Stage the connection promises concurrently and enforce the aggregate tool/catalog limits after they settle so optional integrations still degrade promptly.

AGENTS.md reference: src/AGENTS.md:L17-L19

Useful? React with 👍 / 👎.

// The export core does not filter visibility — it serializes what it is given. A model the
// user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
// client a selector the proxy refuses to route.
const models = rows.filter(row => !row.disabled).map(toExportModel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply Direct-mode visibility to API exports

When the OpenAI provider uses Codex Direct mode, this filter retains native rows even though the CLI's canonical opencodeCatalogFromProxyRows explicitly removes them. API/GUI exports therefore advertise native models using only the proxy admission-key reference, while Direct routing requires the caller's real ChatGPT bearer, so those selections fail; reuse the same catalog visibility derivation for this route.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

Comment thread src/usage/log.ts
}
const promise = readUsageEntriesFullCooperatively(path);
managementUsageReadInflight = { key, promise };
existing?.abort.abort(new Error("management usage read superseded"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep concurrent usage snapshot reads independent

When the usage log is appended while a large cooperative parse is running, a second /api/usage or /api/keys read observes a different revision key and aborts the first request here. The first caller then receives the route's read_failed fallback or empty attribution even though its already-open snapshot was valid, so active traffic can make management requests cancel one another; retain flights per key or allow the older read to finish rather than aborting it.

Useful? React with 👍 / 👎.

Comment on lines +16 to +18
- **CONDITIONALLY-UNBOUNDED**: one dimension is uncapped, cleanup is lazy, historical
key churn accumulates, or growth is limited only by active concurrency/config.
- **UNBOUNDED**: reachable additions have no count, byte, TTL, or replacement bound.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove security triage from the tracked devlog

This tracked _plan unit classifies reachable states as unbounded and records exact source locations and remediation plans while its later completion checklist still identifies open gaps. Publishing these files therefore discloses unreleased findings and pre-disclosure patch planning; keep this material in .tmp/ until fixes ship, then publish only the closed outcome under _fin/.

AGENTS.md reference: AGENTS.md:L61-L68

Useful? React with 👍 / 👎.

Comment thread src/cli/export-command.ts
Comment on lines +166 to +167
const models = exportModelsFromProxyRows(rows, config);
const clientConfig = buildClientConfig(client, { baseUrl: proxyV1BaseUrl(root), models, config });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject exports after visibility removes every model

When /api/models is a valid array but every row is disabled, filtered by Direct-mode visibility, or otherwise absent, exportModelsFromProxyRows returns an empty array and the CLI immediately serializes and may write a seemingly valid config with no usable models. The management route has the same gap despite explicitly stating that empty model blocks must be treated as unavailable; check models.length before building or writing the artifact and return a nonzero/503 result.

Useful? React with 👍 / 👎.

Comment thread src/config.ts
Comment on lines +478 to +480
mcpMaxTools: z.number().int().positive().optional(),
mcpMaxSchemaBytes: z.number().int().positive().optional(),
mcpMaxResultBytes: z.number().int().positive().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new usage and MCP limits

These provider-level MCP limits and the new managementUsageMaxReadBytes setting become accepted public configuration, but the configuration reference added by this commit documents only appOwnedMemoryBudgetMb. Operators encountering catalog/result-limit failures or needing to tune usage-log reads therefore cannot discover the keys, units, or defaults; add them to the English configuration reference and keep the translated tables synchronized.

AGENTS.md reference: AGENTS.md:L163-L164

Useful? React with 👍 / 👎.

Comment thread src/oauth/index.ts
if (tokenRefreshes.get(key) === existing) tokenRefreshes.delete(key);
existing = undefined;
}
if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map OAuth refresh admission to a retryable response

When 32 distinct OAuth refreshes are already active, or when an existing refresh is replaced as stale, these newly introduced retryable errors propagate into handleResponses, whose generic OAuth catch converts them to HTTP 401 authentication_error. Clients consequently treat temporary capacity or owner-staleness as invalid credentials instead of retrying; recognize both errors at the request boundary and return a structured 503 with Retry-After.

AGENTS.md reference: src/AGENTS.md:L17-L20

Useful? React with 👍 / 👎.


/** Back-compat wrapper: callers that only need the wire bytes. */
export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
return prepareCursorRunRequest(request).bytes;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release blob scopes in the compatibility encoder

Every caller of the backward-compatible encodeCursorRunRequest receives only the bytes and loses the blobRequestScope returned by prepareCursorRunRequest, so the sealed scope can never be released. Its blobs remain request-pinned, bypass TTL and app-budget eviction, and repeated legacy calls eventually exhaust the 4,096-entry/64 MiB store; release the scope before returning from this compatibility path or expose an explicit lifecycle without changing the old unpinned behavior.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 46

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/codex/auth-api.ts (1)

272-287: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the two terminal login-state TTLs.

Line 285 hardcodes a 30-second (30_000) delayed deletion for rows created/updated by expireCodexAuthFlow. Line 1472 uses the named CODEX_LOGIN_TERMINAL_TTL_MS constant (300_000 ms, defined at line 97) for the same conceptual purpose: retaining a terminal login-state row long enough for a client polling /api/codex-auth/login-status to observe it. Two different retention windows for the same class of row invite confusion during future tuning of one value without the other, in a codebase whose current review focus is auditing exactly this kind of state retention.

Use CODEX_LOGIN_TERMINAL_TTL_MS in expireCodexAuthFlow too, unless a shorter cancel-specific TTL is intentional; if intentional, name a separate constant instead of the bare 30_000 literal.

🔧 Proposed fix
-    setTimeout(() => { if (codexAuthLoginState.get(id) === owner) codexAuthLoginState.delete(id); }, 30_000);
+    setTimeout(() => { if (codexAuthLoginState.get(id) === owner) codexAuthLoginState.delete(id); }, CODEX_LOGIN_TERMINAL_TTL_MS);

Also applies to: 1462-1473

🤖 Prompt for 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.

In `@src/codex/auth-api.ts` around lines 272 - 287, Update expireCodexAuthFlow to
use the shared CODEX_LOGIN_TERMINAL_TTL_MS constant for delayed deletion,
matching the terminal-row cleanup in the other login-state path. If a shorter
cancellation-specific retention is intentional, replace the bare 30_000 literal
with a separately named constant.
src/server/management/api-key-usage.ts (1)

139-166: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A misconfigured maxReadBytes silently reports zero usage for every key.

readUsageSnapshotForManagement validates its argument and throws on a bad value: if (!Number.isSafeInteger(maxReadBytes) || maxReadBytes <= 0) throw new RangeError(...) (src/usage/log.ts Line 552). The value passed at Line 151 originates from operator configuration — config.managementUsageMaxReadBytes, forwarded at src/server/management/oauth-account-routes.ts Line 490.

The catch at Line 162 is unconditional. So a config value of 0, -1, 1.5, or a non-safe integer produces this outcome:

  1. readUsageSnapshotForManagement throws RangeError on every call.
  2. The catch returns { requests7d: 0, totalRequests: 0 } for every configured id.
  3. No historyTruncated flag is set, and no error surfaces.

GET /api/keys then reports "used zero times" for every key, which is indistinguishable from genuine zero usage. The dashboard shows confidently wrong numbers, and the operator gets no signal that the cause is a config typo. The catch exists to tolerate a missing or unreadable usage log — a programming/configuration error should not ride the same path.

Normalize the limit before the read so a bad value degrades to the documented default instead of zeroing the surface:

🐛 Proposed fix: reject the invalid limit before the tolerant catch
 export async function readApiKeyUsageRollup(configuredIds: string[], maxReadBytes?: number): Promise<ApiKeyUsageSnapshot> {
+  // The snapshot reader throws RangeError on a non-positive / non-safe-integer
+  // limit. The catch below is for a missing or unreadable usage log, not for a
+  // config typo: letting a bad limit fall through would report every key as
+  // "used zero times" with no truncation flag and no error.
+  const effectiveMaxReadBytes = Number.isSafeInteger(maxReadBytes) && (maxReadBytes as number) > 0
+    ? maxReadBytes
+    : undefined;
   // JSON rather than a joined string: ids are only validated as non-empty
   // strings, so `["a\0b","c"]` and `["a","b\0c"]` join to the same value and one
   // config could be served the other's cached rollup.
-  const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
+  const idsKey = JSON.stringify([configuredIds, effectiveMaxReadBytes]);
   const now = Date.now();
   try {
     const observedKey = `${usageLogRevisionKey(currentUsageLogRevision())}|${idsKey}`;
     if (rollupCache?.revisionKey === observedKey && now < rollupCache.expiresAt) {
       return rollupCache.snapshot;
     }
 
-    const snapshot = await readUsageSnapshotForManagement(maxReadBytes);
+    const snapshot = await readUsageSnapshotForManagement(effectiveMaxReadBytes);

If the intended behavior is instead to surface the misconfiguration, validate managementUsageMaxReadBytes at config load and reject it there.

🤖 Prompt for 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.

In `@src/server/management/api-key-usage.ts` around lines 139 - 166, Update
readApiKeyUsageRollup so maxReadBytes is validated or normalized before entering
the tolerant try/catch around readUsageSnapshotForManagement. Invalid values
such as non-positive, non-integer, or unsafe integers must use the documented
default (or be rejected during configuration loading) rather than reaching the
catch and returning zero usage; preserve the catch behavior for missing or
unreadable usage logs.
src/server/management/oauth-account-routes.ts (1)

203-214: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add route-level regression tests for OAuth logout and API-key usage truncation.

  • In tests/oauth-public-surface.test.ts, assert that POST /api/oauth/logout removes the credential and its owned state-store rows.
  • In tests/api-keys-routes.test.ts, assert that bounded GET /api/keys returns historyTruncated: true, and unbounded reads omit the field.
  • The existing tests cover /api/usage and unknown OAuth providers only.
🤖 Prompt for 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.

In `@src/server/management/oauth-account-routes.ts` around lines 203 - 214, Add
route-level regression coverage for the OAuth logout handler to verify POST
/api/oauth/logout removes the provider credential and its owned state-store
rows. Extend API-key route tests to verify bounded GET /api/keys responses set
historyTruncated: true, while unbounded responses omit that field; preserve the
existing /api/usage and unknown-provider tests.

Source: Path instructions

src/codex/project-config-warnings.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated MAX_DIAGNOSTIC_VALUE_BYTES constant across two files. Both src/codex/project-config-warnings.ts and src/lib/crash-guard.ts independently define the identical literal 8 * 1024 byte cap for diagnostic-value truncation, even though both already import truncateRetainedUtf8 from the same shared module src/lib/admission.ts. The shared root cause is the missing shared constant next to that helper.

  • src/codex/project-config-warnings.ts#L6-10: import a shared DIAGNOSTIC_VALUE_MAX_BYTES constant from src/lib/admission.ts instead of declaring MAX_DIAGNOSTIC_VALUE_BYTES locally.
  • src/lib/crash-guard.ts#L210-232: import the same shared constant instead of declaring its own local MAX_DIAGNOSTIC_VALUE_BYTES.
♻️ Proposed consolidation
--- a/src/lib/admission.ts
+++ b/src/lib/admission.ts
@@
+export const DIAGNOSTIC_VALUE_MAX_BYTES = 8 * 1024;
--- a/src/codex/project-config-warnings.ts
+++ b/src/codex/project-config-warnings.ts
-import { truncateRetainedUtf8 } from "../lib/admission";
+import { DIAGNOSTIC_VALUE_MAX_BYTES, truncateRetainedUtf8 } from "../lib/admission";
 const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
 const DIAGNOSTICS_CACHE_TTL_MS = 30_000;
-const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
+const MAX_DIAGNOSTIC_VALUE_BYTES = DIAGNOSTIC_VALUE_MAX_BYTES;
--- a/src/lib/crash-guard.ts
+++ b/src/lib/crash-guard.ts
-import { retainedUtf8Bytes, truncateRetainedUtf8 } from "./admission";
+import { DIAGNOSTIC_VALUE_MAX_BYTES, retainedUtf8Bytes, truncateRetainedUtf8 } from "./admission";
+// ...
-const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
+const MAX_DIAGNOSTIC_VALUE_BYTES = DIAGNOSTIC_VALUE_MAX_BYTES;
🤖 Prompt for 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.

In `@src/codex/project-config-warnings.ts` at line 1, Define and export a shared
DIAGNOSTIC_VALUE_MAX_BYTES constant with the 8 * 1024 cap alongside
truncateRetainedUtf8 in src/lib/admission.ts. Update project-config-warnings.ts
and crash-guard.ts to import and use this shared constant, removing their local
MAX_DIAGNOSTIC_VALUE_BYTES declarations while preserving existing truncation
behavior.
🤖 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
`@devlog/_plan/260801_zero_leak_state_stores/055_background_shell_lifecycle.md`:
- Around line 108-121: The background-shell termination transition must race
child close against the two-second grace timer so SIGKILL is sent when close
does not arrive, then race close against the final bounded two-second wait
before settling. Update the lifecycle symbols implementing `entry.terminating`,
timer cleanup, stdin ending, stream draining, and close/error handling so
`error` records the failure and enters controlled termination without deleting
the entry; delete only after close or a verified pre-registration spawn failure,
and only when `backgroundShells.get(shellId) === entry`, while preserving
bounded settlement and privacy-safe accounting.

In `@docs-site/src/content/docs/guides/opencode.md`:
- Around line 72-75: Insert a blank line between the closing code fence and the
aside terminator `:::` in the documentation block containing the `ocx export`
example, keeping the aside properly closed and satisfying markdownlint MD031.

In `@gui/src/components/apikeys-workspace/ClientConfigPanel.tsx`:
- Around line 147-160: Update the segmented control in ClientConfigPanel’s
CLIENTS.map rendering to use toggle-button semantics instead of radio-group
semantics: remove the radiogroup/radio roles and aria-checked attributes, expose
each button’s selected state with aria-pressed, and preserve the existing client
selection behavior and styling. Update the related client-config-panel tests to
assert the new markup rather than radiogroup and aria-checked semantics.

In `@gui/src/components/MemoryObservabilityCard.tsx`:
- Around line 27-39: Update the continuation store section in the component
rendering the existing count, totalBytes, largestBytes, and oldestAgeMs Stat
rows to surface the new ResponseState metrics: residentCount, spillStubCount,
tombstoneCount, spillPayloadBytes, spillWrites, spillWriteFailures, and
spillReadFailures; include the failure counters as warning indicators where
supported.

In `@gui/src/pages/ApiKeys.tsx`:
- Line 134: Update the keys and models resource definitions to pass validated
cached responses through their resource-layer initialData options, allowing
useDataSurface to be seeded from cache. Remove the page-level keysState.data ??
cachedKeys fallback and use keysState.data as the sole keys source, applying the
same change to the models resource and its consumers.

In `@gui/src/pages/Usage.tsx`:
- Line 248: Remove the unused usage.range.all translation entries from the six
locale resources de.ts, en.ts, ja.ts, ko.ts, ru.ts, and zh.ts; keep Usage.tsx
using usage.range.available for the "all" choice.

In `@gui/tests/usage-layout.test.ts`:
- Around line 124-133: Update the usage layout test setup to set ocx-lang to
"en" before rendering and import the en locale from ../src/i18n/en. In the
polling condition and final assertion, replace hardcoded English text with
en["usage.historyTruncated"], and use en["usage.range.available"] for the button
aria-label selector.

In `@src/cli/export-command.ts`:
- Around line 111-115: Update proxyV1BaseUrl to preserve the parsed root URL
scheme when constructing the /v1 proxy base URL, rather than relying on
opencodeProxyBaseUrl’s HTTP-only result; pass url.protocol through the helper or
construct the URL directly, and add a regression test covering an HTTPS
deps.baseUrl.

In `@src/codex/model-cache.ts`:
- Around line 178-187: Reset lastReconciledGeneration in the full-clear branch
of clearModelCache, alongside cacheBytes and the other cache state resets.
Ensure subsequent low-generation calls to reconcileModelCacheProviders are
processed rather than rejected by the existing high-water-mark check.

In `@src/combos/failover.ts`:
- Around line 80-85: Update reconcileComboTargetCooldowns to identify
targetCooldowns entries whose `${comboId}::${targetKey}` is no longer present in
context.comboTargets, delete those obsolete cooldowns, and return the number
removed. Preserve the existing generation guard and liveComboTargets
reconciliation behavior.

In `@src/combos/resolve.ts`:
- Around line 178-187: The advanceComboAfterFailure retry path must not select
from stale configuration or recreate removed rotation state. Before calling
pickComboTarget, reject stale pick.writerGeneration values or re-resolve the
combo from the current configuration, and ensure every selection-state write in
the round-robin path only occurs when the target is still owned by the live
configuration; add a regression test covering reload between the initial pick
and advanceComboAfterFailure.

In `@src/lib/admission.ts`:
- Around line 60-75: Hoist a single module-level TextEncoder in
src/lib/admission.ts and update retainedUtf8Bytes and utf8Prefix to reuse it
instead of constructing an encoder per character. Preserve the existing UTF-8
byte counting and prefix truncation behavior.

In `@src/lib/app-owned-memory.ts`:
- Around line 209-218: Update enforceAppOwnedMemoryBudget so re-entrant
snapshots are self-describing: initialize inProgressSnapshot with a seeded
snapshot before retainedSnapshots() invokes owner snapshot callbacks, and build
re-entrant/in-progress snapshots with observed data included. Preserve eviction
behavior while ensuring callers can distinguish suppressed or partial snapshots
from a genuine zero-retained-bytes result.

In `@src/lib/config-ownership.ts`:
- Around line 86-111: Extract the duplicated live-owned-path existence check
into a shared helper near listLiveConfigOwnershipRoots, such as
hasLiveOwnedPath(root, ownership). Replace the inline manifest-path checks in
both listLiveConfigOwnershipRoots and reconcileConfigOwnershipRoots with calls
to that helper, preserving their existing behavior.

In `@src/lib/debug-log-buffer.ts`:
- Around line 82-83: Move the newly added imports in src/lib/debug-log-buffer.ts
at lines 82-83 to the top alongside the existing imports. In
src/lib/sidecar-tracker.ts at lines 50-52, move the truncateRetainedUtf8 import
and MAX_DIAGNOSTIC_VALUE_BYTES constant above sidecarEnter and markActivity,
before their use; no other behavior changes are needed.

In `@src/lib/injection-debug-log.ts`:
- Around line 27-35: Update injectionDebugLog to pass the already-truncated
retainedLine to console.log instead of the original line, ensuring console
output respects MAX_DEBUG_LINE_BYTES.

In `@src/oauth/store.ts`:
- Around line 423-433: Update listLiveOAuthAccountKeys to generate each account
key through the existing oauthAccountKey(provider, account.id) helper instead of
duplicating the separator-based string format, while preserving the current
provider filtering and returned key set behavior.

In `@src/providers/quota.ts`:
- Around line 437-458: In the surrounding account-quota lookup flow, extract the
repeated guarded cache-write sequence into a local commit helper that checks
mayCommitAccountQuotaKey, updates accountQuotaCache, and calls
sweepExpiredOnWrite. Replace each success, new-entry, and catch branch’s
duplicated block with the helper while preserving their existing entry values
and return behavior.
- Around line 952-958: Update the response handling in the quota probe so the
returned response uses the same reports filtered by mayCommitProviderQuotaKey as
the cache commit. Ensure both the committed cache entry and the return value
exclude providers rejected by the current writerGeneration, while preserving the
existing invalidationEpoch authority check.
- Around line 345-353: Register sweepExpiredProviderAccountQuotaRows with the
state-store system by importing it into the registrations module and assigning
it to the sweepExpired field of the "provider-quota-history" registration.
Preserve the existing registration configuration so periodic sweepExpired() and
sweepExpiredOnWrite() calls also clear expired account quota entries.

In `@src/responses/spill-store.ts`:
- Around line 252-260: Make the cleanup call after publishNoReplace in the spill
publication flow best-effort: preserve the successful publication and return
result even when unlink(publishTempPath) fails, including ENOENT. Keep
publishNoReplace failures and EEXIST retry behavior unchanged, and ensure
cleanup errors do not reach the outer write-failure handler.
- Around line 171-177: Update the openSync call in the exclusive-copy fsync path
to open destinationPath with write-capable mode "r+" instead of read-only mode
"r", while preserving the existing fsync and closeSync cleanup flow.

In `@src/server/chat-completions.ts`:
- Line 53: Update fail() in responsesSseToChatCompletionsSse to invoke the
existing upstream cancellation path before closing the transformed stream.
Ensure the tracked Responses stream is aborted and turnAdmissionLease is
released even when conversion fails, rather than relying on downstream cancel()
after controller.close().

In `@src/server/claude-messages.ts`:
- Line 515: Propagate the optional AdmissionLease from the logIds/request
handling into anthropicNativePassthrough instead of dropping it on the native
early-return path. Bind that lease to the native SSE stream lifetime and its
abort controller using the existing trackStreamLifetime/registerTurn
integration, preserving cleanup on completion and cancellation. Add a regression
test covering native Anthropic streaming during drain or admission-cap
enforcement.

In `@src/server/index.ts`:
- Around line 373-387: Update the native Claude SSE path in
anthropicNativePassthrough to accept and retain the ActiveTurnLease, then wrap
the tapped stream with trackStreamLifetime so the lease is released only when
the stream completes or is cancelled. Pass the lease from runAdmittedHttpTurn’s
native /v1/messages handling into anthropicNativePassthrough, while preserving
the existing lease-transfer behavior for Responses and Chat Completions.

In `@src/server/lifecycle.ts`:
- Around line 71-82: Add the controller to knownTurnControllers at the start of
registerTurn, before the optional lease binding, so both leased and leaseless
turns are tracked as known controllers. Preserve the existing
bindAbortController behavior for leases, allowing unregisterTurn to avoid
incrementing turnReleaseMisses for intentionally unadmitted turns.

In `@src/server/management-api.ts`:
- Around line 127-144: Update the CatalogGatherBusyError catch block in the
management API dispatcher to return the 503 payload through jsonResponse,
passing the current request and config so corsHeaders(req, config) is included.
Preserve the existing error body, status, and Retry-After header.

In `@src/server/management/config-routes.ts`:
- Line 130: Update both GET and PUT /api/settings responses in config-routes.ts
at lines 130 and 249 to report
resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb) converted from
bytes to megabytes, replacing config.appOwnedMemoryBudgetMb ?? 256. Keep both
responses identical so they reflect the enforced budget and shared default.
- Around line 236-243: Remove the unbounded synchronous enforcement from the PUT
settings response path around configureAppOwnedMemoryBudget and
enforceAppOwnedMemoryBudget. Keep budget configuration and persistence
immediate, then defer or cap enforcement so the request returns without waiting
for all evictions; rely on the existing registerAppOwnedMemorySweepFallback
mechanism to finish convergence.

In `@src/storage/storage-mutation-coordinator.ts`:
- Around line 85-94: Replace the legacy endStorageMutation calls in the
referenced race tests with release() on the AdmissionLease returned by
tryBeginStorageMutation, preserving the existing cleanup behavior. After all
callers are migrated, remove the endStorageMutation function and its export from
the storage mutation coordinator.

In `@tests/active-registry-admission.test.ts`:
- Around line 31-33: Extract the repeated OPENCODEX_HOME temporary-directory
setup, restoration, and cleanup into a shared helper, and update the affected
tests to use it. Ensure the helper restores the original environment value and
removes the temporary directory reliably while leaving test-specific cleanup,
such as stopping the upstream server, in each test’s finally block.
- Around line 29-30: The bound tests duplicate production capacity constants. In
tests/active-registry-admission.test.ts lines 29-30, import and use the exported
active-turn cap from src/server/lifecycle.ts instead of 256, and replace the
literal 16 at line 199 with the exported reservation cap from
src/storage/worker-lifecycle.ts. In tests/cursor-mcp-manager.test.ts lines
155-177, replace 4 * 1024 * 1024 with the exported catalog byte budget from
src/adapters/cursor/mcp-manager.ts or pass an explicit manager limit as the
existing maxTools option does.

In `@tests/api-usage.test.ts`:
- Around line 109-127: Update the test around the existing usage route requests
to change managementUsageMaxReadBytes through the route’s configuration
lifecycle after the first response, then issue a new request and assert its
truncation metadata is recomputed rather than reused from cache. Keep the
existing assertions for repeated-request metadata preservation, and retain the
current test name only if it now covers both cache preservation and limit
invalidation.

In `@tests/app-owned-memory.test.ts`:
- Around line 363-391: Add focused tests for resolveAppOwnedMemoryBudgetBytes
covering valid integer megabyte values at MIN_APP_OWNED_MEMORY_BUDGET_MB and
MAX_APP_OWNED_MEMORY_BUDGET_MB, plus rejected non-integers and out-of-range
inputs returning DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES. Import the three budget
constants and resolver alongside the existing app-owned-memory imports, keeping
the test near the current accounting and enforcement coverage.

In `@tests/cli-export-command.test.ts`:
- Around line 188-197: Update the test "without --out nothing is written to the
real destination path" to direct the command’s configuration environment,
including XDG_CONFIG_HOME or HOME as used by spec.destination, into the temp
directory created by tempDir(). Resolve and assert the expected opencode.json
path there, so the assertion verifies the actual destination rather than an
unrelated directory.

In `@tests/cursor-blob.test.ts`:
- Around line 795-804: Rename the test around releaseCursorBlobRequestScope to
describe request-scope release idempotency: it stores two blobs, seals the
scope, releases it twice, and verifies all pins are freed. Remove references to
stream close, errors, and aborts from the test title while leaving the test body
unchanged.
- Around line 1189-1196: Strengthen the observe-only test around
cursorBlobRetainedStoreSnapshot and cursorBlobMetrics by reading the metrics,
reading the retained-store snapshot again, then asserting the metrics remain
unchanged. Remove the tautological before/after snapshot equality check while
preserving the existing exact count and byte expectations.

In `@tests/cursor-live-transport.test.ts`:
- Around line 63-85: Move cursor blob limit and state cleanup into an afterEach
hook near the test suite’s describe block, calling setCursorBlobLimitsForTests
and resetCursorBlobStateForTests so cleanup runs after assertion failures.
Remove the corresponding inline cleanup calls from the affected test while
preserving its existing assertions and setup.

In `@tests/debug.test.ts`:
- Around line 194-202: Split the shared byte-limit assertion in the
retained-values test so the debug log and injection log remain capped at 16 KiB,
while activityBreadcrumb().note is validated against the 8 KiB limit used by
markActivity and MAX_DIAGNOSTIC_VALUE_BYTES. Keep the existing truncation-marker
and replacement-character checks for all retained values.
- Around line 168-185: Update the test “stale subscriber disposer cannot remove
a replacement registration” to subscribe the same listener twice before
unsubscribing: retain both disposers, call the first to release the shared
registration, create the replacement subscription, then call the second original
disposer for the first time. Keep the existing assertions verifying the
replacement remains active and the subscriber metrics are unchanged.

In `@tests/management-client-config-route.test.ts`:
- Line 22: Add the CI secret-scanner suppression annotation to the synthetic
REAL_LOOKING_KEY fixture in tests/management-client-config-route.test.ts:22,
documenting that it is intentional test data; apply the same annotation to
LIVE_KEY in tests/client-config-export.test.ts:224 so both fake credentials are
consistently ignored.

In `@tests/memory-watchdog.test.ts`:
- Around line 263-278: Reset app-owned memory before this test as well as after
it by adding the existing reset operation to the relevant beforeEach hook in
tests/memory-watchdog.test.ts. Match the symmetric before/after isolation
pattern used in app-owned-memory.test.ts, ensuring
registerDefaultAppOwnedMemoryStores() starts from clean module-level state
before the exact-key assertion runs.

In `@tests/model-visibility-management-api.test.ts`:
- Around line 65-77: Replace the source-text checks in the test with focused
behavioral regression coverage for the affected handlers and startup flows.
Invoke the management and v1 handlers with a catalog gather operation that
throws CatalogGatherBusyError, asserting HTTP 503, the expected response body,
and Retry-After: 1; also exercise catalog prewarm and system-environment
handling to verify the operation is skipped. Reuse the existing subsystem test
helpers and fixtures where available rather than inspecting source strings.

In `@tests/openai-provider-option-startup.test.ts`:
- Around line 127-151: Remove the unused hideTempFromExists option and its
conditional behavior from aclBackupIO, then delete the redundant test scenario
that relies on hiding the temporary path from exists checks. Keep the existing
failure-path coverage through failTempUnlink and isMissingPathError unchanged.

In `@tests/responses-state.test.ts`:
- Around line 979-995: The test’s configured spill I/O does not make snapshot
writes fail, so it does not exercise the stated persistent-failure scenario.
Update the test around setSpillIoForTest and rememberLarge to use an existing or
newly added seam that causes atomicWriteFileAsync snapshot writes to fail
persistently; otherwise revise the test description and assertions to cover the
actual synchronous no-flush behavior.

In `@tests/state-store-sweeper.test.ts`:
- Around line 227-246: Rewrite the test “stale or duplicate generation cannot
delete current keys” to exercise production behavior: register a real store with
registerStateStore({ name, reconcileGeneration }) and invoke
reconcileStateGeneration, following the existing provider-quota late-completion
test pattern. Remove the inline reconcileOwner implementation and direct calls
so the test verifies the stale-generation guard in
src/lib/state-store-sweeper.ts while preserving the expected rows assertions.

---

Outside diff comments:
In `@src/codex/auth-api.ts`:
- Around line 272-287: Update expireCodexAuthFlow to use the shared
CODEX_LOGIN_TERMINAL_TTL_MS constant for delayed deletion, matching the
terminal-row cleanup in the other login-state path. If a shorter
cancellation-specific retention is intentional, replace the bare 30_000 literal
with a separately named constant.

In `@src/codex/project-config-warnings.ts`:
- Line 1: Define and export a shared DIAGNOSTIC_VALUE_MAX_BYTES constant with
the 8 * 1024 cap alongside truncateRetainedUtf8 in src/lib/admission.ts. Update
project-config-warnings.ts and crash-guard.ts to import and use this shared
constant, removing their local MAX_DIAGNOSTIC_VALUE_BYTES declarations while
preserving existing truncation behavior.

In `@src/server/management/api-key-usage.ts`:
- Around line 139-166: Update readApiKeyUsageRollup so maxReadBytes is validated
or normalized before entering the tolerant try/catch around
readUsageSnapshotForManagement. Invalid values such as non-positive,
non-integer, or unsafe integers must use the documented default (or be rejected
during configuration loading) rather than reaching the catch and returning zero
usage; preserve the catch behavior for missing or unreadable usage logs.

In `@src/server/management/oauth-account-routes.ts`:
- Around line 203-214: Add route-level regression coverage for the OAuth logout
handler to verify POST /api/oauth/logout removes the provider credential and its
owned state-store rows. Extend API-key route tests to verify bounded GET
/api/keys responses set historyTruncated: true, while unbounded responses omit
that field; preserve the existing /api/usage and unknown-provider tests.
🪄 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: 77297e39-a1b0-4b40-b481-5ffb73423102

📥 Commits

Reviewing files that changed from the base of the PR and between e1fc0f2 and b04f2ba.

📒 Files selected for processing (187)
  • devlog/_plan/260731_client_config_export/000_plan.md
  • devlog/_plan/260731_client_config_export/001_client_config_survey.md
  • devlog/_plan/260731_client_config_export/002_existing_surface_inventory.md
  • devlog/_plan/260731_client_config_export/003_export_ux_design.md
  • devlog/_plan/260731_client_config_export/010_export_core.md
  • devlog/_plan/260731_client_config_export/020_cli_surface.md
  • devlog/_plan/260731_client_config_export/030_management_api.md
  • devlog/_plan/260731_client_config_export/040_gui_panel.md
  • devlog/_plan/260731_client_config_export/050_docs_and_hardening.md
  • devlog/_plan/260801_zero_leak_state_stores/000_state_store_inventory.md
  • devlog/_plan/260801_zero_leak_state_stores/005_impl_roadmap.md
  • devlog/_plan/260801_zero_leak_state_stores/006_roadmap_audit_synthesis.md
  • devlog/_plan/260801_zero_leak_state_stores/010_continuation_hard_cap.md
  • devlog/_plan/260801_zero_leak_state_stores/020_blob_and_replay_caps.md
  • devlog/_plan/260801_zero_leak_state_stores/030_eviction_mechanisms.md
  • devlog/_plan/260801_zero_leak_state_stores/035_registry_admission_caps.md
  • devlog/_plan/260801_zero_leak_state_stores/040_app_bytes_observability.md
  • devlog/_plan/260801_zero_leak_state_stores/050_translator_stream_bounds.md
  • devlog/_plan/260801_zero_leak_state_stores/055_background_shell_lifecycle.md
  • devlog/_plan/260801_zero_leak_state_stores/060_proxy_benchmark.md
  • devlog/_plan/260801_zero_leak_state_stores/070_close_and_push.md
  • docs-site/astro.config.mjs
  • docs-site/src/content/docs/guides/opencode.md
  • docs-site/src/content/docs/guides/pi.md
  • docs-site/src/content/docs/ja/reference/cli.md
  • docs-site/src/content/docs/ja/reference/configuration.md
  • docs-site/src/content/docs/ko/reference/cli.md
  • docs-site/src/content/docs/ko/reference/configuration.md
  • docs-site/src/content/docs/reference/cli.md
  • docs-site/src/content/docs/reference/configuration.md
  • docs-site/src/content/docs/ru/reference/cli.md
  • docs-site/src/content/docs/ru/reference/configuration.md
  • docs-site/src/content/docs/zh-cn/reference/cli.md
  • docs-site/src/content/docs/zh-cn/reference/configuration.md
  • gui/src/components/MemoryObservabilityCard.tsx
  • gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx
  • gui/src/components/apikeys-workspace/ClientConfigPanel.tsx
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/ApiKeys.tsx
  • gui/src/pages/Usage.tsx
  • gui/src/styles-apikeys-workspace.css
  • gui/tests/apikeys-workspace.test.tsx
  • gui/tests/client-config-panel.test.tsx
  • gui/tests/usage-layout.test.ts
  • src/adapters/anthropic-image-normalize.ts
  • src/adapters/cursor/live-models.ts
  • src/adapters/cursor/live-transport.ts
  • src/adapters/cursor/mcp-manager.ts
  • src/adapters/cursor/native-exec-mcp.ts
  • src/adapters/cursor/native-exec.ts
  • src/adapters/cursor/protobuf-request.ts
  • src/adapters/google-antigravity-replay.ts
  • src/adapters/mimo-free.ts
  • src/claude/inbound-debug.ts
  • src/cli/catalog-prewarm.ts
  • src/cli/export-command.ts
  • src/cli/help.ts
  • src/cli/index.ts
  • src/cli/opencode.ts
  • src/clients/config-export.ts
  • src/codex/account-runtime-state.ts
  • src/codex/account-store.ts
  • src/codex/auth-api.ts
  • src/codex/auth-context.ts
  • src/codex/catalog/aggregation.ts
  • src/codex/catalog/provider-fetch.ts
  • src/codex/main-account-cache.ts
  • src/codex/model-cache.ts
  • src/codex/pool-rotation.ts
  • src/codex/project-config-warnings.ts
  • src/codex/quota.ts
  • src/codex/routing.ts
  • src/codex/shim.ts
  • src/codex/subagent-model-fallback.ts
  • src/codex/websocket-registry.ts
  • src/combos/failover.ts
  • src/combos/request.ts
  • src/combos/resolve.ts
  • src/combos/types.ts
  • src/config.ts
  • src/lib/admission.ts
  • src/lib/app-owned-memory-stores.ts
  • src/lib/app-owned-memory.ts
  • src/lib/config-ownership.ts
  • src/lib/crash-guard.ts
  • src/lib/debug-log-buffer.ts
  • src/lib/errors.ts
  • src/lib/gcp-adc.ts
  • src/lib/injection-debug-log.ts
  • src/lib/sidecar-tracker.ts
  • src/lib/state-store-registrations.ts
  • src/lib/state-store-sweeper.ts
  • src/lib/windows-secret-acl.ts
  • src/oauth/anthropic-routing.ts
  • src/oauth/index.ts
  • src/oauth/store.ts
  • src/oauth/token-guardian.ts
  • src/providers/key-failover.ts
  • src/providers/openai-sidecar.ts
  • src/providers/quota.ts
  • src/responses/spill-store.ts
  • src/responses/state.ts
  • src/router.ts
  • src/server/chat-completions.ts
  • src/server/claude-messages.ts
  • src/server/index.ts
  • src/server/lifecycle.ts
  • src/server/management-api.ts
  • src/server/management-auth.ts
  • src/server/management/api-key-usage.ts
  • src/server/management/combo-routes.ts
  • src/server/management/config-routes.ts
  • src/server/management/logs-usage-routes.ts
  • src/server/management/model-routes.ts
  • src/server/management/oauth-account-routes.ts
  • src/server/management/provider-routes.ts
  • src/server/management/system-routes.ts
  • src/server/management/usage-summary-cache.ts
  • src/server/request-log.ts
  • src/server/responses/compact.ts
  • src/server/responses/core.ts
  • src/server/startup-health-cache.ts
  • src/server/system-env.ts
  • src/server/ws-bridge.ts
  • src/storage/policy-job.ts
  • src/storage/restore-job.ts
  • src/storage/storage-mutation-coordinator.ts
  • src/storage/worker-lifecycle.ts
  • src/tray/windows.ts
  • src/types.ts
  • src/usage/log.ts
  • src/usage/summary.ts
  • src/vision/index.ts
  • structure/02_config-and-codex-home.md
  • structure/05_gui-and-management-api.md
  • tests/active-registry-admission.test.ts
  • tests/anthropic-image-normalize.test.ts
  • tests/api-debug.test.ts
  • tests/api-usage.test.ts
  • tests/app-owned-memory.test.ts
  • tests/claude-inbound-debug.test.ts
  • tests/cli-catalog-prewarm.test.ts
  • tests/cli-export-command.test.ts
  • tests/cli-headless-parity.test.ts
  • tests/client-config-export.test.ts
  • tests/codex-account-store.test.ts
  • tests/codex-auth-api.test.ts
  • tests/codex-auth-context.test.ts
  • tests/codex-main-rotation.test.ts
  • tests/combos.test.ts
  • tests/config.test.ts
  • tests/cursor-blob.test.ts
  • tests/cursor-hardening.test.ts
  • tests/cursor-live-transport.test.ts
  • tests/cursor-mcp-manager.test.ts
  • tests/debug.test.ts
  • tests/gather-routed-models-single-flight.test.ts
  • tests/gcp-adc.test.ts
  • tests/google-antigravity-replay.test.ts
  • tests/helpers/test-budget.ts
  • tests/issue-702-expired-replay-state.test.ts
  • tests/management-client-config-route.test.ts
  • tests/memory-watchdog.test.ts
  • tests/mimo-free-provider.test.ts
  • tests/model-visibility-management-api.test.ts
  • tests/oauth-manual-code.test.ts
  • tests/oauth-refresh.test.ts
  • tests/oauth-store-multi.test.ts
  • tests/openai-provider-option-startup.test.ts
  • tests/provider-account-quota.test.ts
  • tests/responses-state.test.ts
  • tests/server-management-auth.test.ts
  • tests/settings-stream-mode.test.ts
  • tests/shutdown-drain.test.ts
  • tests/state-store-sweeper.test.ts
  • tests/storage-mutation-race.test.ts
  • tests/storage-worker-lifecycle.test.ts
  • tests/usage-log.test.ts
  • tests/usage-summary.test.ts
  • tests/vision-cache.test.ts
  • tests/windows-secret-acl.test.ts
  • tests/windows-tray.test.ts

Comment thread docs-site/src/content/docs/guides/opencode.md
Comment on lines +147 to +160
<div className="segmented awi-clientconfig-segmented" role="radiogroup" aria-label={t("api.clientConfig.clientLabel")}>
{CLIENTS.map(option => (
<button
key={option}
type="button"
role="radio"
aria-checked={client === option}
className={`btn btn-sm${client === option ? " btn-primary" : " btn-ghost"}`}
onClick={() => setClient(option)}
>
{t(CLIENT_LABEL_KEYS[option])}
</button>
))}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The segmented control claims radiogroup semantics without radio keyboard behavior.

Lines 147-159 build a role="radiogroup" whose children are role="radio" buttons. The ARIA radio-group pattern requires one tab stop for the whole group and arrow-key movement between options. This markup gives each option its own tab stop and implements no arrow handling, so a screen-reader user is told "radio button, 1 of 2" and then finds that arrow keys do nothing.

The plan document at devlog/_plan/260731_client_config_export/040_gui_panel.md line 110 permits either pattern. The aria-pressed button group is the cheaper and honest option here.

♿ Proposed fix: use a toolbar of toggle buttons
-      <div className="segmented awi-clientconfig-segmented" role="radiogroup" aria-label={t("api.clientConfig.clientLabel")}>
+      <div className="segmented awi-clientconfig-segmented" role="group" aria-label={t("api.clientConfig.clientLabel")}>
         {CLIENTS.map(option => (
           <button
             key={option}
             type="button"
-            role="radio"
-            aria-checked={client === option}
+            aria-pressed={client === option}
             className={`btn btn-sm${client === option ? " btn-primary" : " btn-ghost"}`}
             onClick={() => setClient(option)}
           >

Note that gui/tests/client-config-panel.test.tsx lines 226-231 assert radiogroup and aria-checked, so update those assertions with the markup.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="segmented awi-clientconfig-segmented" role="radiogroup" aria-label={t("api.clientConfig.clientLabel")}>
{CLIENTS.map(option => (
<button
key={option}
type="button"
role="radio"
aria-checked={client === option}
className={`btn btn-sm${client === option ? " btn-primary" : " btn-ghost"}`}
onClick={() => setClient(option)}
>
{t(CLIENT_LABEL_KEYS[option])}
</button>
))}
</div>
<div className="segmented awi-clientconfig-segmented" role="group" aria-label={t("api.clientConfig.clientLabel")}>
{CLIENTS.map(option => (
<button
key={option}
type="button"
aria-pressed={client === option}
className={`btn btn-sm${client === option ? " btn-primary" : " btn-ghost"}`}
onClick={() => setClient(option)}
>
{t(CLIENT_LABEL_KEYS[option])}
</button>
))}
</div>
🤖 Prompt for 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.

In `@gui/src/components/apikeys-workspace/ClientConfigPanel.tsx` around lines 147
- 160, Update the segmented control in ClientConfigPanel’s CLIENTS.map rendering
to use toggle-button semantics instead of radio-group semantics: remove the
radiogroup/radio roles and aria-checked attributes, expose each button’s
selected state with aria-pressed, and preserve the existing client selection
behavior and styling. Update the related client-config-panel tests to assert the
new markup rather than radiogroup and aria-checked semantics.

Comment on lines 27 to 39
interface ResponseState {
count: number;
residentCount: number;
spillStubCount: number;
tombstoneCount: number;
totalBytes: number;
spillPayloadBytes: number;
largestBytes: number;
oldestAgeMs: number;
spillWrites: number;
spillWriteFailures: number;
spillReadFailures: number;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial

Surface the new spill/tombstone metrics in the panel, or note that this is staged for later.

The ResponseState interface now carries residentCount, spillStubCount, tombstoneCount, spillPayloadBytes, spillWrites, spillWriteFailures, and spillReadFailures. The rendered "continuation store" section (Lines 474-485) still only shows count, totalBytes, largestBytes, and oldestAgeMs. None of the seven new fields reach the screen.

These new counters look designed to help diagnose spill-store leaks and failures. If the backend now reports them, add corresponding Stat rows (or at minimum surface spillWriteFailures/spillReadFailures as a warning indicator) so operators can act on this data. If display is deliberately deferred to a follow-up change, ignore this note.

🤖 Prompt for 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.

In `@gui/src/components/MemoryObservabilityCard.tsx` around lines 27 - 39, Update
the continuation store section in the component rendering the existing count,
totalBytes, largestBytes, and oldestAgeMs Stat rows to surface the new
ResponseState metrics: residentCount, spillStubCount, tombstoneCount,
spillPayloadBytes, spillWrites, spillWriteFailures, and spillReadFailures;
include the failure counters as warning indicators where supported.

Comment thread gui/src/pages/ApiKeys.tsx
},
claudeCodeEnabled: data.claudeCodeEnabled !== false,
...(data.attributionSince ? { attributionSince: data.attributionSince } : {}),
...(data.historyTruncated === true ? { historyTruncated: true } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Seed useDataSurface with the cached response.

These lines extend the page-level fallback path, keysState.data ?? cachedKeys. The data-surface state still does not know that cached data exists.

Pass validated cached data through the resource initialData option. Then use keysState.data as the single keys source. Apply the same pattern to the models resource.

Based on learnings: use the resource-layer initialData option and avoid page-level cache hydration logic.

Also applies to: 186-186

🤖 Prompt for 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.

In `@gui/src/pages/ApiKeys.tsx` at line 134, Update the keys and models resource
definitions to pass validated cached responses through their resource-layer
initialData options, allowing useDataSurface to be seeded from cache. Remove the
page-level keysState.data ?? cachedKeys fallback and use keysState.data as the
sole keys source, applying the same change to the models resource and its
consumers.

Source: Learnings

Comment thread gui/src/pages/Usage.tsx
<div className="usage-segmented" role="group" aria-label={t("usage.title")}>
{(["all", "30d", "7d"] as Range[]).map(choice => {
const label = t(`usage.range.${choice}`);
const label = choice === "all" ? t("usage.range.available") : t(`usage.range.${choice}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the new range key is defined in every locale bundle, and check the old key for orphaning.
set -euo pipefail

echo "== usage.range.* definitions per locale =="
fd -t f -e ts . gui/src/i18n --exec rg -n '"usage\.range\.[a-z0-9]+"' {}

echo "== references to usage.range.all =="
rg -n 'usage\.range\.all' gui/src

Repository: lidge-jun/opencodex

Length of output: 1054


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locale files and exact key definitions =="
for f in gui/src/i18n/*.ts; do
  printf '%s\n' "$f"
  rg -n -F '"usage.range.available"' "$f"
  rg -n -F '"usage.range.all"' "$f" || true
done

echo "== non-definition references to usage.range.all =="
rg -n -F 'usage.range.all' gui/src --glob '*.ts' --glob '*.tsx' \
  | rg -v '^\s*gui/src/i18n/' || true

Repository: lidge-jun/opencodex

Length of output: 433


🏁 Script executed:

#!/bin/bash
set -u

echo "== remaining locale key definitions =="
for f in gui/src/i18n/ja.ts gui/src/i18n/ko.ts gui/src/i18n/ru.ts gui/src/i18n/zh.ts; do
  printf '%s\n' "$f"
  rg -n -F '"usage.range.available"' "$f" || true
  rg -n -F '"usage.range.all"' "$f" || true
done

echo "== references outside locale definitions =="
rg -n -F 'usage.range.all' gui/src --glob '*.ts' --glob '*.tsx' \
  | rg -v '^gui/src/i18n/' || true

Repository: lidge-jun/opencodex

Length of output: 619


Remove the unused usage.range.all translations. usage.range.available exists in all six locale files, and line 248 is correctly localized. Delete the unreferenced usage.range.all entries from gui/src/i18n/de.ts, en.ts, ja.ts, ko.ts, ru.ts, and zh.ts.

🤖 Prompt for 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.

In `@gui/src/pages/Usage.tsx` at line 248, Remove the unused usage.range.all
translation entries from the six locale resources de.ts, en.ts, ja.ts, ko.ts,
ru.ts, and zh.ts; keep Usage.tsx using usage.range.available for the "all"
choice.

Source: Path instructions

Comment on lines +263 to +278

test("GET system memory includes privacy-safe appOwnedBytes scalars", async () => {
registerDefaultAppOwnedMemoryStores();
const req = new Request("http://127.0.0.1:10100/api/system/memory");
const body = await (await handleManagementAPI(req, new URL(req.url), config()))!.json() as {
appOwnedBytes: ReturnType<typeof appOwnedBytesSnapshot>;
};
expect(Object.keys(body.appOwnedBytes.stores).sort()).toEqual([
"antigravity_replay", "claude_debug", "crash_ring", "cursor_blobs", "image_normalize",
"injection_debug", "model_cache", "provider_debug", "request_log", "responses_continuation",
"usage_summary", "vision_descriptions",
]);
expect(Object.values(body.appOwnedBytes.stores).flatMap(snapshot => Object.values(snapshot))
.every(value => value === null || typeof value === "number")).toBe(true);
expect(body.appOwnedBytes.observedInFlight).toEqual({});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset app-owned memory before the test, not only after, so the exact-key assertion cannot inherit foreign registrations.

Lines 270-274 assert the complete sorted key set of body.appOwnedBytes.stores. The reset added at Lines 40-41 runs in the cleanup hook, so isolation depends on a prior test in this file having already executed. registerRetainedStore (src/lib/app-owned-memory.ts Lines 87-92) writes into module-level state that any imported module can populate, and Bun supports randomized test order (bun test --randomize). If this test runs first, or if a transitively imported module registers a store at import time, the assertion fails with a confusing extra key.

Add the reset to a beforeEach as well. tests/app-owned-memory.test.ts Lines 64-73 already uses the symmetric before/after pattern; match it here.

💚 Suggested change
+beforeEach(() => {
+  resetAppOwnedMemoryForTests();
+  resetDebugLogBufferForTests();
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("GET system memory includes privacy-safe appOwnedBytes scalars", async () => {
registerDefaultAppOwnedMemoryStores();
const req = new Request("http://127.0.0.1:10100/api/system/memory");
const body = await (await handleManagementAPI(req, new URL(req.url), config()))!.json() as {
appOwnedBytes: ReturnType<typeof appOwnedBytesSnapshot>;
};
expect(Object.keys(body.appOwnedBytes.stores).sort()).toEqual([
"antigravity_replay", "claude_debug", "crash_ring", "cursor_blobs", "image_normalize",
"injection_debug", "model_cache", "provider_debug", "request_log", "responses_continuation",
"usage_summary", "vision_descriptions",
]);
expect(Object.values(body.appOwnedBytes.stores).flatMap(snapshot => Object.values(snapshot))
.every(value => value === null || typeof value === "number")).toBe(true);
expect(body.appOwnedBytes.observedInFlight).toEqual({});
});
beforeEach(() => {
resetAppOwnedMemoryForTests();
resetDebugLogBufferForTests();
});
test("GET system memory includes privacy-safe appOwnedBytes scalars", async () => {
registerDefaultAppOwnedMemoryStores();
const req = new Request("http://127.0.0.1:10100/api/system/memory");
const body = await (await handleManagementAPI(req, new URL(req.url), config())!).json() as {
appOwnedBytes: ReturnType<typeof appOwnedBytesSnapshot>;
};
expect(Object.keys(body.appOwnedBytes.stores).sort()).toEqual([
"antigravity_replay", "claude_debug", "crash_ring", "cursor_blobs", "image_normalize",
"injection_debug", "model_cache", "provider_debug", "request_log", "responses_continuation",
"usage_summary", "vision_descriptions",
]);
expect(Object.values(body.appOwnedBytes.stores).flatMap(snapshot => Object.values(snapshot))
.every(value => value === null || typeof value === "number")).toBe(true);
expect(body.appOwnedBytes.observedInFlight).toEqual({});
});
🤖 Prompt for 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.

In `@tests/memory-watchdog.test.ts` around lines 263 - 278, Reset app-owned memory
before this test as well as after it by adding the existing reset operation to
the relevant beforeEach hook in tests/memory-watchdog.test.ts. Match the
symmetric before/after isolation pattern used in app-owned-memory.test.ts,
ensuring registerDefaultAppOwnedMemoryStores() starts from clean module-level
state before the exact-key assertion runs.

Comment on lines +65 to +77
test("catalog busy maps management and v1 models to 503 startup to warn-skip and system-env to skip", async () => {
const management = await Bun.file(new URL("../src/server/management-api.ts", import.meta.url)).text();
const server = await Bun.file(new URL("../src/server/index.ts", import.meta.url)).text();
const prewarm = await Bun.file(new URL("../src/cli/catalog-prewarm.ts", import.meta.url)).text();
const systemEnv = await Bun.file(new URL("../src/server/system-env.ts", import.meta.url)).text();
for (const source of [management, server]) {
expect(source).toContain("CatalogGatherBusyError");
expect(source).toContain('"catalog_busy"');
expect(source).toContain('"Retry-After": "1"');
}
expect(prewarm).toContain("startup discovery skipped");
expect(systemEnv).toContain('(error as { code?: unknown }).code === "catalog_busy"');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Replace source-text assertions with behavioral regression tests.

This test passes if each file contains the expected strings in unrelated code. It does not verify that CatalogGatherBusyError returns HTTP 503, includes Retry-After: 1, or skips prewarm and system-environment work.

Invoke the affected handlers with a catalog gather operation that throws CatalogGatherBusyError. Assert the actual status, body, headers, and skipped behavior.

As per path instructions, tests/** requires that “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.”

🤖 Prompt for 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.

In `@tests/model-visibility-management-api.test.ts` around lines 65 - 77, Replace
the source-text checks in the test with focused behavioral regression coverage
for the affected handlers and startup flows. Invoke the management and v1
handlers with a catalog gather operation that throws CatalogGatherBusyError,
asserting HTTP 503, the expected response body, and Retry-After: 1; also
exercise catalog prewarm and system-environment handling to verify the operation
is skipped. Reuse the existing subsystem test helpers and fixtures where
available rather than inspecting source strings.

Source: Path instructions

Comment on lines +127 to +151
function aclBackupIO(options: {
failTempUnlink?: () => boolean;
hideTempFromExists?: boolean;
vanishAfterHarden?: boolean;
} = {}): OpenAiTierBackupIO {
return {
exists: path => options.hideTempFromExists && path.endsWith(".tmp") ? false : existsSync(path),
read: path => readFileSync(path),
createExclusive: path => { writeFileSync(path, new Uint8Array(), { flag: "wx", mode: 0o600 }); },
write: (path, bytes) => writeFileSync(path, bytes),
harden: path => {
chmodSync(path, 0o600);
windowsAcl.hardenSecretPath(path, { required: true });
if (options.vanishAfterHarden) unlinkSync(path);
},
publishNoReplace: (temp, backup) => linkSync(temp, backup),
truncate: path => truncateSync(path, 0),
unlink: path => {
if (path.endsWith(".tmp") && options.failTempUnlink?.()) {
throw Object.assign(new Error("injected temp unlink failure"), { code: "EPERM" });
}
unlinkSync(path);
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'isMissingPathError' -A 12 src/config.ts
rg -n 'io\.exists\(' src/config.ts

Repository: lidge-jun/opencodex

Length of output: 5071


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- src/config.ts ---'
sed -n '300,430p' src/config.ts
printf '%s\n' '--- tests/openai-provider-option-startup.test.ts ---'
sed -n '280,385p' tests/openai-provider-option-startup.test.ts
printf '%s\n' '--- aclBackupIO references ---'
rg -n 'aclBackupIO|hideTempFromExists|failTempUnlink|vanishAfterHarden' tests/openai-provider-option-startup.test.ts
printf '%s\n' '--- static temp-existence check ---'
python3 - <<'PY'
from pathlib import Path
source = Path("src/config.ts").read_text()
start = source.index("export function backupConfigBeforeOpenAiTierMigration")
end = source.index("\nexport ", start + 1)
body = source[start:end]
print("io.exists(temp) present:", "io.exists(temp)" in body)
print("io.exists(source) present:", "io.exists(source)" in body)
print("io.exists(backup) present:", "io.exists(backup)" in body)
PY

Repository: lidge-jun/opencodex

Length of output: 11283


Remove the ineffective hideTempFromExists case.

backupConfigBeforeOpenAiTierMigration calls io.exists only for source and backup. It does not call io.exists(temp). Cleanup uses the caught error code through isMissingPathError, so lines 327-355 exercise the same path as lines 309-325. Remove hideTempFromExists and this redundant scenario, or add a production path that checks the temporary file before testing it.

🤖 Prompt for 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.

In `@tests/openai-provider-option-startup.test.ts` around lines 127 - 151, Remove
the unused hideTempFromExists option and its conditional behavior from
aclBackupIO, then delete the redundant test scenario that relies on hiding the
temporary path from exists checks. Keep the existing failure-path coverage
through failTempUnlink and isMissingPathError unchanged.

Comment thread tests/responses-state.test.ts
Comment on lines +227 to +246
test("stale or duplicate generation cannot delete current keys", () => {
const rows = new Set(["live", "removed"]);
let last = 0;
const reconcileOwner = (next: GenerationContext): number => {
if (next.generation <= last) return 0;
let removed = 0;
for (const row of rows) {
if (next.providerNames.has(row)) continue;
rows.delete(row);
removed += 1;
}
last = next.generation;
return removed;
};

reconcileOwner(context(1, { providerNames: new Set(["live"]) }));
expect([...rows]).toEqual(["live"]);
reconcileOwner(context(1, { providerNames: new Set() }));
expect([...rows]).toEqual(["live"]);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test does not exercise the sweeper's stale-generation guard.

reconcileOwner at Line 230 is a closure declared inside this test. It is never registered with registerStateStore and never invoked through the imported reconcileStateGeneration. Line 242 and Line 244 call reconcileOwner(...) directly.

This means the test only verifies that the next.generation <= last guard written inline in the test body works — a guard the test itself wrote and controls. It does not verify that src/lib/state-store-sweeper.ts enforces the same guard for real owners. If the production staleness check in reconcileStateGeneration regresses (for example, an owner processes a stale or duplicate generation and deletes a live key), this test keeps passing, because it never calls the production function.

Compare with the tests at Line 216-225, Line 248-285, and Line 342-399 in this same file, which register real stores and invoke reconcileStateGeneration/sweepExpiredOnWrite — that is the pattern this test should follow. Rewrite the test to register a store via registerStateStore({ name: ..., reconcileGeneration: ... }) and call the real reconcileStateGeneration (mirroring the "provider-quota late completion" test's structure), so a regression in the actual staleness guard fails this test.

As per path instructions for tests/**: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem." This test's title claims that coverage but its body never calls the subsystem it names.

🤖 Prompt for 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.

In `@tests/state-store-sweeper.test.ts` around lines 227 - 246, Rewrite the test
“stale or duplicate generation cannot delete current keys” to exercise
production behavior: register a real store with registerStateStore({ name,
reconcileGeneration }) and invoke reconcileStateGeneration, following the
existing provider-quota late-completion test pattern. Remove the inline
reconcileOwner implementation and direct calls so the test verifies the
stale-generation guard in src/lib/state-store-sweeper.ts while preserving the
expected rows assertions.

Source: Path instructions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment thread devlog/_plan/260801_zero_leak_state_stores/055_background_shell_lifecycle.md Outdated
Comment thread src/combos/resolve.ts
Comment on lines +178 to 187
noteComboFailure(pick.comboId, pick.target, pick.writerGeneration);
coolComboTarget(pick.comboId, pick.target, {
...options,
writerGeneration: pick.writerGeneration,
});
return pickComboTarget(config, pick.comboId, {
exclude: pick.attempted,
eligible: target => !isComboTargetInCooldown(pick.comboId, target, options.now)
&& (options.eligible?.(target) ?? true),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent a stale failure retry from rebuilding removed rotation state.

advanceComboAfterFailure can receive a ComboPick and config from before reconciliation. Line 183 then calls pickComboTarget with that old config, but Line 93 captures the current generation. The round-robin path at Lines 103-121 can therefore write an active target that was removed from the live configuration as current-generation state.

Reject the retry when pick.writerGeneration is stale, or re-resolve the combo from the current configuration before selecting the next target. Also guard every selection-state write with the target ownership check. Add a regression test for a reload between the first pick and advanceComboAfterFailure.

🤖 Prompt for 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.

In `@src/combos/resolve.ts` around lines 178 - 187, The advanceComboAfterFailure
retry path must not select from stale configuration or recreate removed rotation
state. Before calling pickComboTarget, reject stale pick.writerGeneration values
or re-resolve the combo from the current configuration, and ensure every
selection-state write in the round-robin path only occurs when the target is
still owned by the live configuration; add a regression test covering reload
between the initial pick and advanceComboAfterFailure.

Comment thread src/lib/admission.ts
Comment on lines +60 to +75
export function retainedUtf8Bytes(value: string): number {
return new TextEncoder().encode(value).byteLength;
}

function utf8Prefix(value: string, maxBytes: number): string {
if (maxBytes <= 0) return "";
let bytes = 0;
let result = "";
for (const character of value) {
const size = retainedUtf8Bytes(character);
if (bytes + size > maxBytes) break;
result += character;
bytes += size;
}
return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse a single TextEncoder instead of allocating one per character.

retainedUtf8Bytes at Line 61 creates new TextEncoder() on every call. utf8Prefix at Line 69 calls retainedUtf8Bytes(character) once per Unicode code point in the input. For a string that exceeds a 16 KiB or 32 KiB cap (the debug-line and Claude-inbound-row limits documented in 035_registry_admission_caps.md), this constructs thousands of TextEncoder instances in a single truncation call. This module is the shared primitive for multiple high-volume retained rings (debug logs, injection logs, crash traces, Claude inbound metadata), so the allocation cost is paid repeatedly across the codebase.

Hoist one module-level encoder and reuse it in both functions.

♻️ Proposed fix to reuse a single encoder
+const utf8Encoder = new TextEncoder();
+
 export function retainedUtf8Bytes(value: string): number {
-  return new TextEncoder().encode(value).byteLength;
+  return utf8Encoder.encode(value).byteLength;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function retainedUtf8Bytes(value: string): number {
return new TextEncoder().encode(value).byteLength;
}
function utf8Prefix(value: string, maxBytes: number): string {
if (maxBytes <= 0) return "";
let bytes = 0;
let result = "";
for (const character of value) {
const size = retainedUtf8Bytes(character);
if (bytes + size > maxBytes) break;
result += character;
bytes += size;
}
return result;
}
const utf8Encoder = new TextEncoder();
export function retainedUtf8Bytes(value: string): number {
return utf8Encoder.encode(value).byteLength;
}
function utf8Prefix(value: string, maxBytes: number): string {
if (maxBytes <= 0) return "";
let bytes = 0;
let result = "";
for (const character of value) {
const size = retainedUtf8Bytes(character);
if (bytes + size > maxBytes) break;
result += character;
bytes += size;
}
return result;
}
🤖 Prompt for 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.

In `@src/lib/admission.ts` around lines 60 - 75, Hoist a single module-level
TextEncoder in src/lib/admission.ts and update retainedUtf8Bytes and utf8Prefix
to reuse it instead of constructing an encoder per character. Preserve the
existing UTF-8 byte counting and prefix truncation behavior.

Comment thread src/server/index.ts
Comment on lines +373 to +387
async function runAdmittedHttpTurn(req: Request, work: (lease: ActiveTurnLease) => Promise<Response>): Promise<Response> {
const lease = tryAdmitTurn();
if (!lease) return serverBusyResponse(req, "active turns");
let response: Response;
try {
response = await work(lease);
} catch (error) {
lease.release();
throw error;
}
if (!lease.isTransferred()) {
lease.release();
}
return response;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the streaming handlers call a transfer method on turnAdmissionLease before
# returning, and inspect the lease API surface in admission.ts.
rg -n "isTransferred|transfer\(" src/lib/admission.ts 2>/dev/null
rg -n "turnAdmissionLease" -B2 -A10 src/server/responses/core.ts src/server/claude-messages.ts src/server/chat-completions.ts 2>/dev/null

Repository: lidge-jun/opencodex

Length of output: 13252


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- admission API ---'
fd -i 'admission' src
rg -n -C 12 "interface AdmissionLease|type AdmissionLease|class .*Lease|isTransferred|transfer\(" src

printf '%s\n' '--- stream lifetime helper ---'
rg -n -C 18 "function trackStreamLifetime|const trackStreamLifetime|trackStreamLifetime" src/server src/lib

printf '%s\n' '--- handler return paths ---'
rg -n -C 8 "return new Response|return .*Response|return upstream|trackStreamLifetime" \
  src/server/responses/core.ts src/server/claude-messages.ts src/server/chat-completions.ts

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- lifecycle lease transfer and stream cleanup ---'
sed -n '20,75p' src/server/lifecycle.ts
sed -n '116,165p' src/server/lifecycle.ts

printf '%s\n' '--- Claude streaming and delegated paths ---'
sed -n '300,370p' src/server/claude-messages.ts
sed -n '670,730p' src/server/claude-messages.ts

printf '%s\n' '--- Chat Completions delegation and return ---'
sed -n '130,180p' src/server/chat-completions.ts

printf '%s\n' '--- Responses stream/non-stream return paths ---'
sed -n '1795,1835p' src/server/responses/core.ts
sed -n '1948,1965p' src/server/responses/core.ts
sed -n '2008,2023p' src/server/responses/core.ts
sed -n '2088,2110p' src/server/responses/core.ts
sed -n '2558,2575p' src/server/responses/core.ts

Repository: lidge-jun/opencodex

Length of output: 18359


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path("src/server/responses/core.ts"),
    Path("src/server/claude-messages.ts"),
    Path("src/server/chat-completions.ts"),
]
for path in files:
    text = path.read_text()
    print(f"--- {path} ---")
    for m in re.finditer(r"trackStreamLifetime\((.*?)\)", text, re.S):
        start = text.count("\n", 0, m.start()) + 1
        call = " ".join(m.group(0).split())
        print(f"line {start}: lease={'turnAdmissionLease' in call or 'options.turnAdmissionLease' in call}; {call[:240]}")
    for m in re.finditer(r"return new Response\((.*?)\)", text, re.S):
        start = text.count("\n", 0, m.start()) + 1
        call = " ".join(m.group(0).split())
        if "text/event-stream" in call or "tapAnthropicSse" in call or "sse" in call.lower():
            print(f"stream return line {start}: tracked={'trackStreamLifetime' in call}; {call[:240]}")
PY

Repository: lidge-jun/opencodex

Length of output: 1780


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- Responses native stream registration ---'
sed -n '1635,1750p' src/server/responses/core.ts
rg -n -C 10 "markNativePassthroughSseResponse|registerTurn\(" src/server/responses/core.ts

printf '%s\n' '--- Claude handler entry and direct passthrough selection ---'
sed -n '470,555p' src/server/claude-messages.ts
rg -n -C 12 "native|passthrough|handleClaudeMessages|tapAnthropicSseForLog" src/server/claude-messages.ts

printf '%s\n' '--- Claude translated stream return ---'
sed -n '730,755p' src/server/claude-messages.ts

printf '%s\n' '--- Chat translated stream return ---'
sed -n '220,250p' src/server/chat-completions.ts

printf '%s\n' '--- route call sites ---'
sed -n '660,775p' src/server/index.ts

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

index = Path("src/server/index.ts").read_text()
claude = Path("src/server/claude-messages.ts").read_text()
core = Path("src/server/responses/core.ts").read_text()
lifecycle = Path("src/server/lifecycle.ts").read_text()

checks = {
    "messages route admits and forwards lease": (
        "runAdmittedHttpTurn(req, async turnAdmissionLease" in index
        and "handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease })" in index
    ),
    "native Claude branch is reachable": (
        "if (isRec(anthropicBody) && wantsNativePassthrough(req, config, anthropicBody.model))" in claude
        and "return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, \"/v1/messages\");" in claude
    ),
    "native Claude SSE returns without lease registration": (
        "return new Response(tapAnthropicSseForLog(upstream.body, logCtx, finalize, bodyGuard)" in claude
        and "trackStreamLifetime" not in claude[claude.index("async function anthropicNativePassthrough"):claude.index("async function anthropicNativePassthrough") + 12000]
    ),
    "Responses routed streams transfer through tracking": (
        "trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease)" in core
        and "registerTurn(ac, lease)" in lifecycle
    ),
    "admission wrapper releases untransferred leases": (
        "if (!lease.isTransferred())" in index
        and "lease.release();" in index
    ),
}
for name, result in checks.items():
    print(f"{'PASS' if result else 'FAIL'}: {name}")
if not all(checks.values()):
    raise SystemExit(1)
PY

printf '%s\n' '--- Claude imports and exact native function signature ---'
sed -n '1,35p' src/server/claude-messages.ts
sed -n '280,350p' src/server/claude-messages.ts

Repository: lidge-jun/opencodex

Length of output: 5421


Keep the admission lease for native Claude SSE streams.

anthropicNativePassthrough drops turnAdmissionLease and returns tapAnthropicSseForLog(...) without registerTurn or trackStreamLifetime (src/server/claude-messages.ts:286-349). runAdmittedHttpTurn therefore releases the lease at src/server/index.ts:383-385 while the native /v1/messages SSE stream remains open. Pass the lease into anthropicNativePassthrough and wrap the tapped stream with trackStreamLifetime so release occurs on completion or cancellation. Routed Responses and Chat Completions streams already transfer the lease through handleResponses.

🤖 Prompt for 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.

In `@src/server/index.ts` around lines 373 - 387, Update the native Claude SSE
path in anthropicNativePassthrough to accept and retain the ActiveTurnLease,
then wrap the tapped stream with trackStreamLifetime so the lease is released
only when the stream completes or is cancelled. Pass the lease from
runAdmittedHttpTurn’s native /v1/messages handling into
anthropicNativePassthrough, while preserving the existing lease-transfer
behavior for Responses and Chat Completions.

Comment thread src/server/lifecycle.ts
Comment on lines +71 to +82
export function registerTurn(ac: AbortController, lease?: AdmissionLease): void {
if (lease && "bindAbortController" in lease) (lease as ActiveTurnLease).bindAbortController(ac);
}
export function unregisterTurn(ac: AbortController): void {
const lease = activeTurns.get(ac);
if (!lease) {
if (knownTurnControllers.has(ac)) return;
turnReleaseMisses += 1;
return;
}
lease.release();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Leaseless turns will falsely inflate the releaseMisses leak signal.

registerTurn at Line 71 is a no-op when lease is not supplied. trackStreamLifetime at Line 120 accepts lease as optional, and the plan doc 035_registry_admission_caps.md (Line 202) states that warmup frames "do not consume a turn lease" — so there is at least one documented call path that reaches registerTurn(ac, undefined).

When such a controller finishes, unregisterTurn at Line 74 finds it in neither activeTurns nor knownTurnControllers, and increments turnReleaseMisses at Line 78. This is the same counter the plan doc calls "the leak signal" (releaseMisses is the leak signal under the foreign-owner rule). Every leaseless/warmup turn will therefore register as a false leak, masking real foreign-owner releases in activeRegistryMetrics().

Add ac to knownTurnControllers inside registerTurn regardless of whether a lease is present, so unregisterTurn treats it as a known, intentionally-unadmitted controller instead of a release miss.

🐛 Proposed fix
 export function registerTurn(ac: AbortController, lease?: AdmissionLease): void {
+  knownTurnControllers.add(ac);
   if (lease && "bindAbortController" in lease) (lease as ActiveTurnLease).bindAbortController(ac);
 }

Based on learnings from the plan doc's own statement that "warmup frames do not consume a turn lease" (devlog/_plan/260801_zero_leak_state_stores/035_registry_admission_caps.md), this scenario is an expected, documented code path rather than a hypothetical edge case.

Also applies to: 120-122

🤖 Prompt for 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.

In `@src/server/lifecycle.ts` around lines 71 - 82, Add the controller to
knownTurnControllers at the start of registerTurn, before the optional lease
binding, so both leased and leaseless turns are tracked as known controllers.
Preserve the existing bindAbortController behavior for leases, allowing
unregisterTurn to avoid incrementing turnReleaseMisses for intentionally
unadmitted turns.

Comment on lines 85 to 94
export function endStorageMutation(codexHome?: string): void {
slots.delete(slotKey(codexHome));
const key = slotKey(codexHome);
const slot = slots.get(key);
if (!slot) {
releaseMisses += 1;
return;
}
slots.delete(key);
slot.lease.release();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find any remaining caller of endStorageMutation outside this file.
rg -n --type=ts -C3 '\bendStorageMutation\s*\(' --glob '!src/storage/storage-mutation-coordinator.ts'

Repository: lidge-jun/opencodex

Length of output: 157


Remove endStorageMutation

tests/storage-mutation-race.test.ts:22 and tests/storage-mutation-race.test.ts:195 still use this legacy API. Update the tests to release the AdmissionLease returned by tryBeginStorageMutation, then remove the export.

endStorageMutation identifies a slot only by its release-time codexHome. A stale caller can therefore release a newer lease for the same resolved home. Production callers already use identity-checked lease release.

🤖 Prompt for 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.

In `@src/storage/storage-mutation-coordinator.ts` around lines 85 - 94, Replace
the legacy endStorageMutation calls in the referenced race tests with release()
on the AdmissionLease returned by tryBeginStorageMutation, preserving the
existing cleanup behavior. After all callers are migrated, remove the
endStorageMutation function and its export from the storage mutation
coordinator.

@lidge-jun
lidge-jun force-pushed the codex/client-config-export branch 2 times, most recently from b04f2ba to a6efb2e Compare August 1, 2026 13:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 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 `@docs-site/src/content/docs/guides/pi.md`:
- Around line 65-66: Insert one blank line between the closing code fence and
the following Starlight aside marker in docs-site/src/content/docs/guides/pi.md
at lines 65-66 and docs-site/src/content/docs/guides/opencode.md at lines 74-75.
- Line 3: Update the page description metadata to replace “writes” with the
side-effect-free verb “generates” or “prints,” accurately reflecting the
behavior of the ocx export command described in the guide.
- Around line 121-122: The documentation incorrectly guarantees that exports
cannot contain an empty model list. Update the statement around the running
opencodex proxy and ocx export flow to acknowledge that empty or disabled-only
/api/models responses may produce empty exports, unless the corresponding CLI
and management API guards are implemented.

In `@docs-site/src/content/docs/reference/cli.md`:
- Line 442: The CLI reference must describe Pi’s top-level providers value as a
keyed object, with only its nested models value represented as an array. Update
docs-site/src/content/docs/reference/cli.md lines 442-442 and synchronize the
same correction in docs-site/src/content/docs/ja/reference/cli.md lines 336-336,
docs-site/src/content/docs/ko/reference/cli.md lines 383-383,
docs-site/src/content/docs/ru/reference/cli.md lines 376-376, and
docs-site/src/content/docs/zh-cn/reference/cli.md lines 327-327.

In `@gui/tests/client-config-panel.test.tsx`:
- Around line 210-220: Add coverage in the client configuration panel tests for
both branches of the Copy JSON action: stub navigator.clipboard.writeText,
verify successful copying contains the rendered OPENCODE_ENVELOPE.config JSON
and announces success, then make the stub reject and verify the polite live
region announces failure. Reuse the existing mountPanel, button, and unmount
patterns.
- Around line 222-234: Update the segmented radiogroup in ClientConfigPanel to
implement roving tabindex: keep only the checked radio tabbable, handle
Left/Right Arrow keys to move focus between the OpenCode and Pi options, and
update client accordingly. Extend the existing client switch test to verify Tab
focus behavior and Arrow-key focus/selection updates.

In `@src/cli/export-command.ts`:
- Around line 61-65: Remove the local hasContextLimit-based counting in the CLI
export flow and add a shared summarizer in config-export.ts that derives
modelCount and modelsWithoutLimits from the serialized document. Update both the
export command and the model-routes API to use this summarizer, ensuring
degraded counts describe emitted bytes and cannot drift from the serializer’s
authoritative limit rule.

In `@src/clients/config-export.ts`:
- Around line 191-203: Rename the shared OpencodeCatalogModel type reference
used by exportModelLabel to a client-neutral export model type, and rename
OPENCODE_PROVIDER_ID to a client-neutral provider constant while updating both
OpenCode and buildPiClientConfig usages. Preserve the existing provider value
and labeling behavior; retain OpenCode-specific aliases only if required for
compatibility.
- Around line 149-158: Update buildPiClientConfig to support custom headers and,
when ExportContext.config indicates a non-loopback bind, emit
x-opencodex-api-key using $OPENCODEX_API_KEY; omit or disable the automatic
apiKey/Authorization header in that branch while preserving current loopback
behavior.
- Around line 107-113: Update buildPiClientConfig to use the live ctx.config
when constructing Pi exports: preserve apiKey for loopback configurations, and
for non-loopback configurations emit the x-opencodex-api-key header through Pi’s
headers support instead. Ensure the exported openai-completions configuration
receives the remote admission header while retaining existing loopback behavior.

In `@src/server/management/model-routes.ts`:
- Around line 255-266: The model catalog error response currently exposes
arbitrary upstream error text. Update the catch block in the client-config route
to log the full error server-side while returning only the stable “model catalog
unavailable” message, preserving CatalogGatherBusyError handling and the 503
response. Update the assertion and stub body in
gui/tests/client-config-panel.test.tsx at line 183 to expect the stable message;
tests/management-client-config-route.test.ts line 236 also requires the same
assertion update while preserving the 503 and absent-config checks.
- Around line 200-208: The build flow should preserve generated config types
instead of returning unknown. Update ExportClientSpec.build and
buildClientConfig to return OpencodeGeneratedConfig | PiGeneratedConfig, then
narrow document by client in summarizeExportedModels before accessing the
corresponding provider fields, removing the unchecked casts while preserving
both empty-model and non-empty-model behavior.
- Around line 271-275: Update the buildClientConfig call in the model-routes
configuration flow to derive the proxy URL from the request authority: use
url.protocol, url.hostname, and the effective protocol default port (80 for HTTP
or 443 for HTTPS) when url.port is absent, preserving IPv6 host formatting.
Avoid config.hostname and config.port for this exported URL, and add coverage
for remote hosts, default ports, HTTPS, and wildcard binds.

In `@tests/client-config-export.test.ts`:
- Around line 150-221: The Pi serializer tests lack coverage for non-loopback
hostname handling. After confirming the behavior implemented in
buildPiClientConfig, add a focused test within the existing “Pi serializer”
describe block that constructs piConfig with ctx(config containing hostname
"0.0.0.0") and asserts the expected admission/header behavior, matching the
equivalent opencodeConfig test without changing unrelated cases.
🪄 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: 948a99d6-3395-4326-a749-abb607954e09

📥 Commits

Reviewing files that changed from the base of the PR and between b04f2ba and a6efb2e.

📒 Files selected for processing (37)
  • devlog/_plan/260731_client_config_export/000_plan.md
  • devlog/_plan/260731_client_config_export/001_client_config_survey.md
  • devlog/_plan/260731_client_config_export/002_existing_surface_inventory.md
  • devlog/_plan/260731_client_config_export/003_export_ux_design.md
  • devlog/_plan/260731_client_config_export/010_export_core.md
  • devlog/_plan/260731_client_config_export/020_cli_surface.md
  • devlog/_plan/260731_client_config_export/030_management_api.md
  • devlog/_plan/260731_client_config_export/040_gui_panel.md
  • devlog/_plan/260731_client_config_export/050_docs_and_hardening.md
  • docs-site/astro.config.mjs
  • docs-site/src/content/docs/guides/opencode.md
  • docs-site/src/content/docs/guides/pi.md
  • docs-site/src/content/docs/ja/reference/cli.md
  • docs-site/src/content/docs/ko/reference/cli.md
  • docs-site/src/content/docs/reference/cli.md
  • docs-site/src/content/docs/ru/reference/cli.md
  • docs-site/src/content/docs/zh-cn/reference/cli.md
  • gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx
  • gui/src/components/apikeys-workspace/ClientConfigPanel.tsx
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/ApiKeys.tsx
  • gui/src/styles-apikeys-workspace.css
  • gui/tests/client-config-panel.test.tsx
  • src/cli/export-command.ts
  • src/cli/help.ts
  • src/cli/index.ts
  • src/cli/opencode.ts
  • src/clients/config-export.ts
  • src/server/management/model-routes.ts
  • tests/cli-export-command.test.ts
  • tests/client-config-export.test.ts
  • tests/management-client-config-route.test.ts

@@ -0,0 +1,122 @@
---
title: Pi
description: Use any routed model from Pi — ocx export writes a custom provider block for Pi's models.json, wired to the running proxy.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a side-effect-free verb in the page description.

Line 3 says that ocx export “writes” a block for models.json. Lines 58-60 state that the command never writes the real Pi configuration and requires manual merging. Change “writes” to “generates” or “prints” so page metadata matches the CLI behavior.

As per path instructions, user-facing documentation must stay synchronized with actual CLI behavior.

Proposed wording
-description: Use any routed model from Pi — ocx export writes a custom provider block for Pi's models.json, wired to the running proxy.
+description: Use any routed model from Pi — ocx export generates a custom provider block for Pi's models.json, wired to the running proxy.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Use any routed model from Pi — ocx export writes a custom provider block for Pi's models.json, wired to the running proxy.
description: Use any routed model from Pi — ocx export generates a custom provider block for Pi's models.json, wired to the running proxy.
🤖 Prompt for 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.

In `@docs-site/src/content/docs/guides/pi.md` at line 3, Update the page
description metadata to replace “writes” with the side-effect-free verb
“generates” or “prints,” accurately reflecting the behavior of the ocx export
command described in the guide.

Source: Path instructions

Comment thread docs-site/src/content/docs/guides/pi.md
Comment on lines +121 to +122
A running opencodex proxy (`ocx start`) and Pi installed. `ocx export` reads the live catalog
through the proxy's management API, so a config can never be emitted with an empty model list.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm that CLI and management API callers reject an empty live catalog.
rg -n -C 8 'buildClientConfig|/api/models|models[[:space:]]*\.length|empty|no models' \
  src/cli src/server tests || true

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- export implementation and callers ---'
rg -n -C 12 'normalizeExportModels|buildClientConfig|/api/models|ocx export|export.*models' \
  src/clients/config-export.ts src/cli src/server tests/cli-export-command.test.ts tests/codex-catalog.test.ts

printf '%s\n' '--- focused export test sections ---'
sed -n '1,330p' tests/cli-export-command.test.ts

printf '%s\n' '--- documentation context ---'
sed -n '105,135p' docs-site/src/content/docs/guides/pi.md

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- export command control flow ---'
sed -n '68,185p' src/cli/export-command.ts

printf '%s\n' '--- model endpoint implementations ---'
rg -n -C 18 'pathname.*api/models|"/api/models"|api/models|handleManagementAPI' \
  src/server src/cli tests/cli-export-command.test.ts tests/codex-catalog.test.ts \
  | head -n 500

printf '%s\n' '--- Pi guide context ---'
sed -n '112,130p' docs-site/src/content/docs/guides/pi.md

printf '%s\n' '--- focused empty-array references ---'
rg -n -C 6 'fakeProxy\(\[\]\)|empty.*model|models.*empty|empty.*catalog|rows.*length|models\.length' \
  src/cli src/server tests/cli-export-command.test.ts tests/client-config-export.test.ts

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- management API model route ---'
rg -n -C 10 'fetchAllModels|allModels|modelsResponse|selected-models|pathname|url.pathname' \
  src/server/management-api.ts src/server/management src/server/index.ts \
  | grep -E 'api/models|fetchAllModels|allModels|modelsResponse|selected-models|^[^:]+:[0-9]+-' \
  | head -n 300

printf '%s\n' '--- standalone empty-catalog verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

export_command = Path("src/cli/export-command.ts").read_text()
serializer = Path("src/clients/config-export.ts").read_text()
export_tests = Path("tests/client-config-export.test.ts").read_text()
cli_tests = Path("tests/cli-export-command.test.ts").read_text()

command = export_command[export_command.index("export async function handleExportCommand"):
                 export_command.index("export const EXPORT_USAGE")]

assert "if (!Array.isArray(rows))" in command
assert "const models = exportModelsFromProxyRows(rows, config);" in command
assert "buildClientConfig(client, { baseUrl: proxyV1BaseUrl(root), models, config })" in command
assert not re.search(r"models\.length\s*===?\s*0|models\.length\s*<\s*1|!models\.length", command)

normalize = serializer[serializer.index("export function normalizeExportModels"):
                    serializer.index("export interface PiModelEntry")]
assert "return unique.sort" in normalize
assert "for (const model of models)" in normalize

# The relevant transformations preserve an empty array.
rows = []
unique = []
assert unique == rows
assert {"models": unique}["models"] == []

assert "an empty catalog still yields a structurally valid document" in export_tests
assert "models: []" in export_tests
assert "a non-array /api/models payload is an error" in cli_tests

print("empty /api/models array -> exportModelsFromProxyRows -> buildClientConfig is accepted")
print("OpenCode model map: {}")
print("Pi model array: []")
print("No empty-model guard exists in handleExportCommand")
PY

Repository: lidge-jun/opencodex

Length of output: 4463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact /api/models response path ---'
sed -n '100,145p' src/server/management/model-routes.ts
sed -n '220,245p' src/server/management/model-routes.ts

printf '%s\n' '--- fetchAllModels empty-result behavior ---'
sed -n '155,205p' src/server/management/shared.ts

printf '%s\n' '--- empty serializer test with line numbers ---'
sed -n '320,338p' tests/client-config-export.test.ts

Repository: lidge-jun/opencodex

Length of output: 6615


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- complete model-row construction ---'
sed -n '125,225p' src/server/management/model-routes.ts

printf '%s\n' '--- management client-config path ---'
sed -n '232,315p' src/server/management/model-routes.ts

printf '%s\n' '--- native row source ---'
rg -n -C 12 'function nativeModelRows|const nativeModelRows|nativeModelRows\(' \
  src/server/management/model-routes.ts src | head -n 250

Repository: lidge-jun/opencodex

Length of output: 21823


🏁 Script executed:

#!/bin/bash
set -euo pipefail

nl -ba src/server/management/model-routes.ts | sed -n '232,285p'
nl -ba src/clients/config-export.ts | sed -n '269,287p;321,335p'
nl -ba tests/client-config-export.test.ts | sed -n '328,335p'

Repository: lidge-jun/opencodex

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

awk 'NR >= 232 && NR <= 285 { printf "%6d  %s\n", NR, $0 }' src/server/management/model-routes.ts
awk 'NR >= 269 && NR <= 287 || NR >= 321 && NR <= 335 { printf "%6d  %s\n", NR, $0 }' src/clients/config-export.ts
awk 'NR >= 328 && NR <= 335 { printf "%6d  %s\n", NR, $0 }' tests/client-config-export.test.ts

Repository: lidge-jun/opencodex

Length of output: 4953


Reject empty exports before serialization.

ocx export accepts an empty or disabled-only /api/models response. The CLI checks only that the response is an array (src/cli/export-command.ts:162-167), then the serializers emit models: {} for OpenCode or models: [] for Pi (src/clients/config-export.ts:269-285, 321-322). The management API has the same gap (src/server/management/model-routes.ts:270-275). Add an empty-model guard and test it, or reword docs-site/src/content/docs/guides/pi.md:121-122 to allow an empty model list.

🤖 Prompt for 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.

In `@docs-site/src/content/docs/guides/pi.md` around lines 121 - 122, The
documentation incorrectly guarantees that exports cannot contain an empty model
list. Update the statement around the running opencodex proxy and ocx export
flow to acknowledge that empty or disabled-only /api/models responses may
produce empty exports, unless the corresponding CLI and management API guards
are implemented.

Source: Path instructions


| Flag | Action |
| --- | --- |
| `--client <opencode\|pi>` | Required. Selects the client dialect: opencode's keyed `provider` object or Pi's `providers` array. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the actual Pi provider shape in every CLI reference.

All five CLI reference pages describe Pi’s top-level providers value as an array. The shared serializer emits a keyed object (Record<string, PiProviderBlock>). Only the nested models field is an array. Users following these tables can create an invalid Pi configuration.

  • docs-site/src/content/docs/reference/cli.md#L442-L442: describe Pi’s keyed providers object and identify models as the array.
  • docs-site/src/content/docs/ja/reference/cli.md#L336-L336: replace the providers array description with a keyed object description.
  • docs-site/src/content/docs/ko/reference/cli.md#L383-L383: replace the providers array description with a keyed object description.
  • docs-site/src/content/docs/ru/reference/cli.md#L376-L376: replace the providers array description with a keyed object description.
  • docs-site/src/content/docs/zh-cn/reference/cli.md#L327-L327: replace the providers array description with a keyed object description.

As per path instructions, the English source and all translated documentation must remain synchronized with the actual export schema.

📍 Affects 5 files
  • docs-site/src/content/docs/reference/cli.md#L442-L442 (this comment)
  • docs-site/src/content/docs/ja/reference/cli.md#L336-L336
  • docs-site/src/content/docs/ko/reference/cli.md#L383-L383
  • docs-site/src/content/docs/ru/reference/cli.md#L376-L376
  • docs-site/src/content/docs/zh-cn/reference/cli.md#L327-L327
🤖 Prompt for 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.

In `@docs-site/src/content/docs/reference/cli.md` at line 442, The CLI reference
must describe Pi’s top-level providers value as a keyed object, with only its
nested models value represented as an array. Update
docs-site/src/content/docs/reference/cli.md lines 442-442 and synchronize the
same correction in docs-site/src/content/docs/ja/reference/cli.md lines 336-336,
docs-site/src/content/docs/ko/reference/cli.md lines 383-383,
docs-site/src/content/docs/ru/reference/cli.md lines 376-376, and
docs-site/src/content/docs/zh-cn/reference/cli.md lines 327-327.

Source: Path instructions

Comment on lines +210 to +220
test("no-key state is informational and leaves copy and download enabled", async () => {
stubRoute(() => Response.json(OPENCODE_ENVELOPE));
const { root, container } = await mountPanel({ hasKeys: false });

expect(container.querySelector(".awi-clientconfig-nokey")?.textContent)
.toContain("OPENCODEX_OPENCODE_API_KEY has no key behind it yet");
expect(button(container, "Copy JSON").disabled).toBe(false);
expect(button(container, "Download").disabled).toBe(false);

await act(async () => { root.unmount(); });
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test exercises the copy action, so both clipboard branches are unverified.

This test asserts only that the "Copy JSON" button is enabled at line 216. ClientConfigPanel.copyJson calls navigator.clipboard.writeText(json) and then announces either api.clientConfig.copiedAnnounce or, on rejection, api.clientConfig.copyFailed. Neither branch is covered, and navigator.clipboard is never stubbed in beforeEach. Copy is one of the panel's two primary actions, and the failure announcement is the only feedback a user gets when the clipboard is unavailable.

Add a test that stubs navigator.clipboard.writeText and asserts the copied bytes equal the rendered JSON, plus one that rejects and asserts the failure announcement:

💚 Proposed test
test("copy writes the rendered JSON and announces success, then failure", async () => {
  stubRoute(() => Response.json(OPENCODE_ENVELOPE));
  const written: string[] = [];
  let reject = false;
  Object.defineProperty(globalThis.navigator, "clipboard", {
    configurable: true,
    value: {
      writeText: async (text: string) => {
        if (reject) throw new Error("denied");
        written.push(text);
      },
    },
  });

  const { root, container } = await mountPanel();
  await act(async () => { button(container, "Copy JSON").click(); });
  expect(JSON.parse(written[0]!)).toEqual(OPENCODE_ENVELOPE.config);
  expect(container.querySelector(".sr-only[aria-live='polite']")!.textContent)
    .toContain("copied to the clipboard");

  reject = true;
  await act(async () => { button(container, "Copy JSON").click(); });
  expect(container.querySelector(".sr-only[aria-live='polite']")!.textContent)
    .toContain("Could not copy");

  await act(async () => { root.unmount(); });
});
🤖 Prompt for 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.

In `@gui/tests/client-config-panel.test.tsx` around lines 210 - 220, Add coverage
in the client configuration panel tests for both branches of the Copy JSON
action: stub navigator.clipboard.writeText, verify successful copying contains
the rendered OPENCODE_ENVELOPE.config JSON and announces success, then make the
stub reject and verify the polite live region announces failure. Reuse the
existing mountPanel, button, and unmount patterns.

Comment on lines +191 to +203
/**
* Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
* provider suffix is what makes two same-named models from different upstreams
* distinguishable in a client's model picker.
*/
function exportModelLabel(model: OpencodeCatalogModel): string {
const providerLabel = model.native ? "native" : (model.provider ?? "routed");
const id = model.id ?? model.namespaced;
if (model.displayName && model.displayName.length > 0) {
return `${model.displayName} (${providerLabel})`;
}
return `${id} (${providerLabel})`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Naming: OpenCode-prefixed identifiers are reused by the client-neutral core.

exportModelLabel (Line 196) is typed to accept OpencodeCatalogModel even though it is the shared label rule for both OpenCode and Pi (buildPiClientConfig calls it at Line 325). Similarly, OPENCODE_PROVIDER_ID (Line 64) is reused as Pi's provider container key at Line 339. Both are functionally correct today (the provider key is genuinely the same string, "opencodex", for both clients), but the OpenCode-specific names make the shared core's cross-client intent less obvious to a future reader working only on the Pi path.

Rename the shared type/constant to a client-neutral name (for example ExportCatalogModel and EXPORT_PROVIDER_ID), or add a short comment at each reuse site noting the name is intentionally shared.

♻️ Illustrative rename (only the shared surface, keep OpenCode-specific aliases if needed for back-compat)
-export const OPENCODE_PROVIDER_ID = "opencodex";
+/** Provider key owned by this project; the only key any exporter ever emits. */
+export const EXPORT_PROVIDER_ID = "opencodex";
+/** `@deprecated` use EXPORT_PROVIDER_ID */
+export const OPENCODE_PROVIDER_ID = EXPORT_PROVIDER_ID;

Also applies to: 339-345

🤖 Prompt for 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.

In `@src/clients/config-export.ts` around lines 191 - 203, Rename the shared
OpencodeCatalogModel type reference used by exportModelLabel to a client-neutral
export model type, and rename OPENCODE_PROVIDER_ID to a client-neutral provider
constant while updating both OpenCode and buildPiClientConfig usages. Preserve
the existing provider value and labeling behavior; retain OpenCode-specific
aliases only if required for compatibility.

Comment on lines +200 to +208
function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
if (client === "opencode") {
const models = (document as OpencodeGeneratedConfig).provider[OPENCODE_PROVIDER_ID].models;
const entries = Object.values(models);
return { modelCount: entries.length, modelsWithoutLimits: entries.filter(entry => entry.limit === undefined).length };
}
const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does buildClientConfig always emit the provider block, even with zero models?
ast-grep outline src/clients/config-export.ts --items all
rg -nP --type=ts -C10 'export function buildClientConfig' src/clients/config-export.ts
rg -nP --type=ts -C6 'OPENCODE_PROVIDER_ID\s*\]\s*=|provider(s)?\s*:\s*\{' src/clients/config-export.ts

Repository: lidge-jun/opencodex

Length of output: 5138


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- config export implementation ---'
sed -n '220,350p' src/clients/config-export.ts
printf '%s\n' '--- route implementation and call sites ---'
sed -n '150,225p' src/server/management/model-routes.ts
rg -n -C6 'summarizeExportedModels|buildClientConfig|client-config' src/server/management src/clients
printf '%s\n' '--- tests for empty model exports ---'
rg -n -C8 'empty|models:\s*\[\]|normalizeExportModels|buildClientConfig|summarizeExportedModels' --glob '*.test.ts' src

Repository: lidge-jun/opencodex

Length of output: 17426


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

config = Path("src/clients/config-export.ts").read_text()
route = Path("src/server/management/model-routes.ts").read_text()

# Extract the relevant source expressions and evaluate the empty-model shape
block = re.search(
    r"function buildOpencodeClientConfig\(ctx: ExportContext\): OpencodeGeneratedConfig \{(?P<body>.*?)\n\}",
    config,
    re.S,
)
if not block:
    raise SystemExit("buildOpencodeClientConfig not found")
body = block.group("body")
required = [
    "normalizeExportModels(ctx.models)",
    "return { $schema: OPENCODE_CONFIG_SCHEMA, provider: { [OPENCODE_PROVIDER_ID]: block } };",
]
for text in required:
    if text not in body:
        raise SystemExit(f"missing expected expression: {text}")

# The builder always constructs the provider map with the provider key.
empty_document = {
    "$schema": "https://opencode.ai/config.json",
    "provider": {"opencodex": {"models": {}}},
}
models = empty_document["provider"]["opencodex"]["models"]
summary = {
    "modelCount": len(models),
    "modelsWithoutLimits": sum(1 for entry in models.values() if "limit" not in entry),
}
print("builder_return_contains_provider_key=true")
print(f"empty_opencode_model_count={summary['modelCount']}")
print(f"empty_opencode_models_without_limits={summary['modelsWithoutLimits']}")

# Check whether the route passes the builder result directly.
if "summarizeExportedModels(client, document)" in route:
    print("route_passes_document_directly=true")
else:
    print("route_passes_document_directly=false")
PY

Repository: lidge-jun/opencodex

Length of output: 304


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- export types and builders ---'
sed -n '130,190p' src/clients/config-export.ts
sed -n '345,380p' src/clients/config-export.ts
printf '%s\n' '--- route imports ---'
sed -n '80,115p' src/server/management/model-routes.ts
printf '%s\n' '--- all buildClientConfig consumers ---'
rg -n -C5 'buildClientConfig\(' --glob '*.ts' .

Repository: lidge-jun/opencodex

Length of output: 14391


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

config = Path("src/clients/config-export.ts").read_text()
route = Path("src/server/management/model-routes.ts").read_text()

spec = re.search(
    r"export interface ExportClientSpec\s*\{(?P<body>.*?)\n\}",
    config,
    re.S,
)
print("export_client_spec_found=" + str(bool(spec)).lower())
if spec:
    print(spec.group("body").strip())

signature = re.search(
    r"export function buildClientConfig\(client: ExportClientId, ctx: ExportContext\): ([^{]+)\{",
    config,
)
print("build_client_config_return_type=" + (signature.group(1).strip() if signature else "not found"))

print("route_build_result_is_explicitly_unknown=" + str("const document = buildClientConfig" in route and "document: unknown" not in route).lower())
print("summarizer_accepts_unknown=" + str("document: unknown" in route).lower())
PY

Repository: lidge-jun/opencodex

Length of output: 755


Preserve generated config types in buildClientConfig.

buildOpencodeClientConfig and buildPiClientConfig always emit their provider block, including when models is empty. The empty-model path does not cause summarizeExportedModels to throw.

However, ExportClientSpec.build and buildClientConfig return unknown, which forces unchecked casts in src/server/management/model-routes.ts:200-206. Return OpencodeGeneratedConfig | PiGeneratedConfig and narrow the document by client before accessing its provider fields.

🤖 Prompt for 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.

In `@src/server/management/model-routes.ts` around lines 200 - 208, The build flow
should preserve generated config types instead of returning unknown. Update
ExportClientSpec.build and buildClientConfig to return OpencodeGeneratedConfig |
PiGeneratedConfig, then narrow document by client in summarizeExportedModels
before accessing the corresponding provider fields, removing the unchecked casts
while preserving both empty-model and non-empty-model behavior.

Comment thread src/server/management/model-routes.ts
Comment on lines +271 to 275
const document = buildClientConfig(requested, {
baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
models,
config,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# What does probeHostname do with 0.0.0.0 / :: ?
rg -nP --type=ts -C8 'function probeHostname' src/clients/config-export.ts
rg -nP --type=ts -C3 'hostname' src/clients/config-export.ts

Repository: lidge-jun/opencodex

Length of output: 1136


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- probeHostname definition and imports ---'
rg -n --type=ts -C12 'probeHostname|opencodeProxyBaseUrl' src/clients src/server
printf '%s\n' '--- route implementation ---'
sed -n '220,285p' src/server/management/model-routes.ts
printf '%s\n' '--- related tests ---'
rg -n -C8 'client-config|baseUrl|hostname|default port|url.port' tests src/server/management

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- definition and import ---'
rg -n --type=ts -g '*.ts' '^(export )?(function|const) probeHostname|probeHostname' src/clients/config-export.ts src
printf '%s\n' '--- route ---'
sed -n '230,280p' src/server/management/model-routes.ts
printf '%s\n' '--- focused tests ---'
rg -n -C6 --glob 'tests/*client-config*' 'baseUrl|hostname|port|client-config'

Repository: lidge-jun/opencodex

Length of output: 5563


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- probeHostname implementation ---'
sed -n '45,85p' src/server/proxy-liveness.ts
printf '%s\n' '--- route response continuation ---'
sed -n '260,295p' src/server/management/model-routes.ts
printf '%s\n' '--- client-config test files ---'
find tests -maxdepth 1 -type f -iname '*client*config*' -print
printf '%s\n' '--- focused test references ---'
rg -n -C8 'api/client-config|baseUrl|filename|destination' tests/management-client-config-route.test.ts tests 2>/dev/null | head -n 240

Repository: lidge-jun/opencodex

Length of output: 22835


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- management API URL construction ---'
rg -n -C8 --type=ts 'handleManagementAPI|new URL\\(|req.url|url = ' src/server src/index.ts src 2>/dev/null | rg -v 'tests|node_modules' | head -n 260
printf '%s\n' '--- export endpoint derivation ---'
sed -n '80,125p' src/cli/export-command.ts
printf '%s\n' '--- config export helpers ---'
sed -n '115,140p' src/clients/config-export.ts
printf '%s\n' '--- URL port behavior ---'
node - <<'JS'
for (const value of [
  "http://gui.example/api/client-config?client=opencode",
  "https://gui.example/api/client-config?client=opencode",
  "http://gui.example:8080/api/client-config?client=opencode",
]) {
  const url = new URL(value);
  console.log(JSON.stringify({ value, hostname: url.hostname, port: url.port }));
}
JS

Repository: lidge-jun/opencodex

Length of output: 3561


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- management handler declarations and call sites ---'
rg -n -C5 --type=ts 'handleManagementAPI' src
printf '%s\n' '--- URL creation near management dispatch ---'
rg -n -C6 --type=ts 'new URL\\(req\\.url|new URL\\(request\\.url|req\\.url' src/server src
printf '%s\n' '--- request URL handling in server entrypoints ---'
rg -n -C8 --type=ts 'management|/api/' src/server/index.ts src/server.ts src/index.ts 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 4032


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '410,458p' src/server/index.ts
printf '%s\n' '--- management origin logic ---'
sed -n '1,135p' src/server/management-api.ts

Repository: lidge-jun/opencodex

Length of output: 9979


🏁 Script executed:

#!/bin/bash
set -eu
rg -n -C4 --type=ts 'new URL|const url|url:' src/server/index.ts | head -n 180
sed -n '300,345p' src/server/index.ts

Repository: lidge-jun/opencodex

Length of output: 3102


🏁 Script executed:

#!/bin/bash
set -eu
node - <<'JS'
function probeHostname(hostname) {
  const trimmed = (hostname ?? "").trim();
  if (!trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") return "127.0.0.1";
  if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
  return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
}
function currentBase(urlText, configPort, configHostname) {
  const url = new URL(urlText);
  return `http://${probeHostname(configHostname)}:${Number(url.port) || configPort}/v1`;
}
function requestBase(urlText) {
  const url = new URL(urlText);
  const port = Number(url.port) || (url.protocol === "https:" ? 443 : 80);
  return `${url.protocol}//${url.hostname}:${port}/v1`;
}
for (const input of [
  ["http://gui.example/api/client-config?client=opencode", 10100, "127.0.0.1"],
  ["https://gui.example/api/client-config?client=opencode", 10100, "127.0.0.1"],
  ["http://gui.example:8080/api/client-config?client=opencode", 10100, "127.0.0.1"],
  ["http://gui.example:8080/api/client-config?client=opencode", 10100, "0.0.0.0"],
]) {
  console.log(JSON.stringify({
    request: input[0],
    current: currentBase(...input),
    requestAuthority: requestBase(input[0]),
  }));
}
JS

Repository: lidge-jun/opencodex

Length of output: 776


Derive the exported proxy URL from the request authority.

At src/server/management/model-routes.ts:272, default ports fall back to config.port, and config.hostname is converted to loopback by probeHostname. A remote GUI therefore receives an unreachable URL such as http://127.0.0.1:10100/v1. HTTPS requests also receive an http URL. Use url.protocol, url.hostname, and the effective default port (80 or 443), while preserving IPv6 formatting. Add coverage for remote hosts, default ports, HTTPS, and wildcard binds.

🤖 Prompt for 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.

In `@src/server/management/model-routes.ts` around lines 271 - 275, Update the
buildClientConfig call in the model-routes configuration flow to derive the
proxy URL from the request authority: use url.protocol, url.hostname, and the
effective protocol default port (80 for HTTP or 443 for HTTPS) when url.port is
absent, preserving IPv6 host formatting. Avoid config.hostname and config.port
for this exported URL, and add coverage for remote hosts, default ports, HTTPS,
and wildcard binds.

Comment on lines +150 to +221
describe("Pi serializer (accept criterion 2)", () => {
test("models is an array keyed by id, not a keyed object", () => {
const provider = piConfig().providers.opencodex!;
expect(Array.isArray(provider.models)).toBe(true);
expect(provider.models.map(model => model.id)).toEqual([
"anthropic/claude-opus-5",
"custom/no-context",
"gpt-5.6-luna",
"tiny/small-ctx",
]);
});

test("provider envelope names the OpenAI-compatible dialect and the env reference", () => {
const provider = piConfig().providers.opencodex!;
expect(provider.baseUrl).toBe(BASE_URL);
expect(provider.api).toBe("openai-completions");
expect(provider.apiKey).toBe(PI_API_KEY_ENV_REF);
expect(provider.apiKey).toBe("$OPENCODEX_API_KEY");
});

test("cost is omitted on every entry — zeros would assert routed models are free", () => {
for (const model of piConfig().providers.opencodex!.models) {
expect(model).not.toHaveProperty("cost");
}
expect(JSON.stringify(piConfig())).not.toContain("cost");
});

test("reasoning is omitted — an effort list is not Pi's boolean", () => {
for (const model of piConfig().providers.opencodex!.models) {
expect(model).not.toHaveProperty("reasoning");
}
});

test("contextWindow and maxTokens are omitted when the context window is unknown", () => {
const entry = piConfig().providers.opencodex!.models.find(model => model.id === "custom/no-context")!;
expect(entry).not.toHaveProperty("contextWindow");
expect(entry).not.toHaveProperty("maxTokens");
expect(entry).toEqual({ id: "custom/no-context", name: "no-context (custom)", input: ["text"] });
});

test("maxTokens uses the schema budget and clamps to a smaller context window", () => {
const models = piConfig().providers.opencodex!.models;
const large = models.find(model => model.id === "gpt-5.6-luna")!;
expect(large.contextWindow).toBe(272_000);
expect(large.maxTokens).toBe(SCHEMA_REQUIRED_OUTPUT_BUDGET);
const small = models.find(model => model.id === "tiny/small-ctx")!;
expect(small.contextWindow).toBe(8_000);
expect(small.maxTokens).toBe(8_000);
});

test("input defaults to text and passes through declared modalities", () => {
const config = piConfig(ctx({
models: [
{ namespaced: "a/plain", provider: "a", id: "plain" },
{ namespaced: "b/multi", provider: "b", id: "multi", inputModalities: ["text", "image"] },
{ namespaced: "c/empty", provider: "c", id: "empty", inputModalities: [] },
],
}));
const models = config.providers.opencodex!.models;
expect(models.find(model => model.id === "a/plain")!.input).toEqual(["text"]);
expect(models.find(model => model.id === "b/multi")!.input).toEqual(["text", "image"]);
expect(models.find(model => model.id === "c/empty")!.input).toEqual(["text"]);
});

test("Pi reuses the OpenCode label rule verbatim", () => {
const piNames = piConfig().providers.opencodex!.models.map(model => model.name).sort();
const opencodeNames = Object.values(opencodeConfig().provider.opencodex!.models)
.map(entry => entry.name)
.sort();
expect(piNames).toEqual(opencodeNames);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Note: no non-loopback test exists for the Pi path.

Every non-loopback / header-branch test (Line 101-111) targets opencodeConfig. There is no equivalent case that builds piConfig(ctx({ config: cfg({ hostname: "0.0.0.0" }) })). This is consistent with buildPiClientConfig never reading ctx.config today, so there is nothing to assert yet. If the verification requested on src/clients/config-export.ts (Lines 149-158, 321-347) confirms Pi needs the same non-loopback admission treatment as OpenCode, add a regression test here alongside the existing Pi describe block.

As per path instructions, "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

🤖 Prompt for 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.

In `@tests/client-config-export.test.ts` around lines 150 - 221, The Pi serializer
tests lack coverage for non-loopback hostname handling. After confirming the
behavior implemented in buildPiClientConfig, add a focused test within the
existing “Pi serializer” describe block that constructs piConfig with ctx(config
containing hostname "0.0.0.0") and asserts the expected admission/header
behavior, matching the equivalent opencodeConfig test without changing unrelated
cases.

Source: Path instructions

Phase 010 of devlog/_plan/260731_client_config_export.

Lifts the OpenCode provider-block builder out of the launcher into a
client-neutral module and adds a Pi serializer beside it. `ocx opencode`
keeps every export it had and its launch path is untouched; the builder
now has one body shared by the launcher and future export surfaces.

The OpenCode output is pinned by a golden captured from the pre-refactor
implementation. Pi emits a models array, always omits `cost` (we have no
price data and zeros would assert "free"), and omits context/maxTokens
when no authoritative context window exists.

Neither serializer ever emits a live key: OpenCode carries
`{env:OPENCODEX_OPENCODE_API_KEY}`, Pi carries `$OPENCODEX_API_KEY`.
Phase 020 of devlog/_plan/260731_client_config_export.

`ocx export --client <opencode|pi>` prints the client config. With --json
stdout is the config and nothing else, so an agent can pipe it; without
it, the JSON is followed by the destination path, the merge warning, the
env line, and a count of models shipping without context limits.

--out writes the file but refuses to clobber an existing one without
--force, since the obvious target is a populated config whose other
providers would be lost.

Disabled rows are filtered here, at the /api/models boundary: the export
core dedupes and sorts but cannot see visibility state. A non-array
models payload is an error rather than an empty-model config.

Also corrects two example paths in the plan docs that tripped
privacy:scan.
Phase 030 of devlog/_plan/260731_client_config_export.

GET /api/client-config?client=<opencode|pi> returns the same bytes the
CLI prints, wrapped in an envelope carrying the destination path, the env
var name, and the model counts the GUI needs to render its states.

The /api/models row logic moved into listManagementModelRows() and both
branches now call it. Copying it would have created a second definition
of which models exist, free to drift from the first; disabled/dedupe
precedence is ~40 lines and only correct in one place.

modelsWithoutLimits is counted from the serialized document rather than
re-derived from input rows, so the number cannot disagree with the bytes
the caller receives. A catalog failure is 503, never a partial 200.
Phase 040 of devlog/_plan/260731_client_config_export.

Renders what GET /api/client-config returns: a segmented OpenCode/Pi
switch, the JSON itself, copy as the primary action, download as the
secondary one, plus the destination path and the env line the config
references.

Download names the file from the server envelope and announces that
nothing changed yet and the file must be merged. A downloaded config
that reads as "applied" is how someone loses the other providers in
their opencode.json.

Rendering it in a real browser caught two defects a static read missed:
a bare .awi-clientconfig-panel selector ties .api-panel's overflow on
specificity and loses, leaving a clipping ancestor between the JSON and
the page scroller; and a max-height on the JSON made a second capped
scroll region on a tab whose invariant is that the model catalog is the
only one. Both are fixed and the layout invariant test covers the second.

Placement is the fallback 003 §6 allows, since the connect-bar rework
has not landed yet.
Phase 050 of devlog/_plan/260731_client_config_export.

The export command shipped in the previous four commits with nothing in
docs-site pointing at it, and Pi had no page at all.

Adds the ocx export section to the CLI reference (English plus the four
translated copies), a section in the opencode guide on getting the
provider block into your own config, and a Pi guide.

Three things the docs have to get right, each verified against the code
while writing: the two clients use DIFFERENT env vars
(OPENCODEX_OPENCODE_API_KEY vs OPENCODEX_API_KEY), a loopback bind needs
no admission key at all since shouldInjectApiAuthHeader is just
!isLoopbackHostname, and an exported config must be merged rather than
replace an existing file.

BYOK is split rather than restated: the proxy admission key is what the
exported config references, provider keys stay in providers.<name>.apiKey
and keep their existing home in the providers guide.

The Pi schema is still unverified against a real install and the guide
says so.
React Doctor flagged the setter after `await` in the panel's effect. The
write was already safe — every result carries its request key and a
superseded run's key no longer matches — but the guard was an
`AbortController.signal.aborted` check, which states "the request was
cancelled", not "this run may no longer write".

An explicit `cancelled` flag set in the cleanup makes the invariant local
to the setter, so a reader (and the linter) can see why a stale resolve
cannot land. The abort stays: it stops the in-flight request, the flag
stops the write.
The headless-parity sweep asserts every GUI management endpoint has a
documented CLI resource. Phase 030 added GET /api/client-config without
registering it, so the sweep went red — correctly: the route and the CLI
command shipped in the same unit and `ocx export` is exactly its mirror.
@lidge-jun
lidge-jun force-pushed the codex/client-config-export branch from a6efb2e to 4521f1d Compare August 1, 2026 14:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
devlog/_plan/260731_client_config_export/020_cli_surface.md (1)

95-104: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Check for live credentials instead of the ocx_ substring.

The human output intentionally contains the placeholder <your ocx_... key> at Lines 44-47. Therefore, “no stdout path in any mode contains ocx_” will reject valid human output.

Assert that stdout does not contain the configured live key, and separately assert that --json contains only the environment reference.

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/020_cli_surface.md` around lines 95
- 104, Update the CLI output assertions in the test plan to stop treating the
“ocx_” substring as forbidden, since human output may include the documented
placeholder. Instead, assert that stdout never contains the configured live
credential, and verify separately that --json output contains only the
environment reference for the key.
🤖 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 `@devlog/_plan/260731_client_config_export/000_plan.md`:
- Around line 96-99: Update the Status section of the roadmap to reflect the
current implementation phase rather than the historical docs-only state.
Summarize the completed implementation phases and include verification evidence
based on the amendments and PR objectives documented in 010_export_core.md;
remove the claim that no implementation has started or no production files were
modified.
- Around line 74-90: The plan must make real-install Pi schema validation a
release gate: update the DONE criteria in 000_plan.md to require successful
validation against an actual ~/.pi/agent/models.json and prevent Pi from stable
CLI, API, and GUI surfaces until it passes; update 010_export_core.md at its
deferred decision to reflect this requirement. If validation cannot be made a
gate, consistently mark Pi export experimental across the plan, all surfaces,
and documentation.

In `@devlog/_plan/260731_client_config_export/003_export_ux_design.md`:
- Around line 154-161: Update the merge-semantics section to describe each
client’s downloaded configuration shape separately: OpenCode includes its
provider block and $schema, while Pi uses a top-level providers object without
$schema. Keep the explicit warning that users must merge these blocks into
existing configuration rather than replace the file.

In `@devlog/_plan/260731_client_config_export/010_export_core.md`:
- Around line 63-66: Make duplicate resolution deterministic in the shared
precondition before either serializer runs: establish a stable precedence order
for rows sharing the same namespaced value, then retain the highest-precedence
row before sorting the deduplicated results. Update the affected export flows
consistently and add a fixture containing conflicting duplicate rows to verify
identical output regardless of input order.
- Around line 109-119: Use the richer existing model-label rule consistently
throughout the plan: update the field table, examples, and golden-fixture
expectations so name values append the appropriate (native|provider|routed)
suffix instead of using only displayName ?? namespaced. Ensure the Pi export
reuses this same label behavior and preserves pre-refactor OpenCode parity.
- Around line 29-58: The export-plan documentation is missing fields and header
behavior already required by the implemented contract. Update ExportModel with
native?: boolean and ExportContext with config?: OcxConfig, document conditional
output of options.headers["x-opencodex-api-key"], and update the CLI surface
snippet to include config; keep the existing bind-mode behavior and client
interfaces unchanged.

In `@devlog/_plan/260731_client_config_export/040_gui_panel.md`:
- Around line 94-102: Reconcile the api.clientConfig key list in the locale
file-change map to the required fifteen keys, removing or correcting the extra
entry while preserving the intended translations. Add a test that iterates over
every listed api.clientConfig.* key and verifies it exists in all six locale
files.

In `@docs-site/src/content/docs/reference/cli.md`:
- Around line 465-469: Clarify the `ocx export` documentation at
docs-site/src/content/docs/reference/cli.md:465-469,
docs-site/src/content/docs/guides/opencode.md:61-64,
docs-site/src/content/docs/guides/pi.md:57-60,
docs-site/src/content/docs/ja/reference/cli.md:358-361,
docs-site/src/content/docs/ko/reference/cli.md:405-408,
docs-site/src/content/docs/ru/reference/cli.md:399-403, and
docs-site/src/content/docs/zh-cn/reference/cli.md:349-352: without `--out`,
output is printed and no canonical client file is modified; with `--out <path>`,
the requested path is written; and `--force` permits replacing any existing
path, including a canonical client configuration, rather than being rejected.
Update each translation consistently.

---

Outside diff comments:
In `@devlog/_plan/260731_client_config_export/020_cli_surface.md`:
- Around line 95-104: Update the CLI output assertions in the test plan to stop
treating the “ocx_” substring as forbidden, since human output may include the
documented placeholder. Instead, assert that stdout never contains the
configured live credential, and verify separately that --json output contains
only the environment reference for the key.
🪄 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: 6122248a-f621-4bd7-9c8e-58cd94c6ee09

📥 Commits

Reviewing files that changed from the base of the PR and between a6efb2e and 4521f1d.

📒 Files selected for processing (38)
  • devlog/_plan/260731_client_config_export/000_plan.md
  • devlog/_plan/260731_client_config_export/001_client_config_survey.md
  • devlog/_plan/260731_client_config_export/002_existing_surface_inventory.md
  • devlog/_plan/260731_client_config_export/003_export_ux_design.md
  • devlog/_plan/260731_client_config_export/010_export_core.md
  • devlog/_plan/260731_client_config_export/020_cli_surface.md
  • devlog/_plan/260731_client_config_export/030_management_api.md
  • devlog/_plan/260731_client_config_export/040_gui_panel.md
  • devlog/_plan/260731_client_config_export/050_docs_and_hardening.md
  • docs-site/astro.config.mjs
  • docs-site/src/content/docs/guides/opencode.md
  • docs-site/src/content/docs/guides/pi.md
  • docs-site/src/content/docs/ja/reference/cli.md
  • docs-site/src/content/docs/ko/reference/cli.md
  • docs-site/src/content/docs/reference/cli.md
  • docs-site/src/content/docs/ru/reference/cli.md
  • docs-site/src/content/docs/zh-cn/reference/cli.md
  • gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx
  • gui/src/components/apikeys-workspace/ClientConfigPanel.tsx
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/ApiKeys.tsx
  • gui/src/styles-apikeys-workspace.css
  • gui/tests/client-config-panel.test.tsx
  • src/cli/export-command.ts
  • src/cli/help.ts
  • src/cli/index.ts
  • src/cli/opencode.ts
  • src/clients/config-export.ts
  • src/server/management/model-routes.ts
  • tests/cli-export-command.test.ts
  • tests/cli-headless-parity.test.ts
  • tests/client-config-export.test.ts
  • tests/management-client-config-route.test.ts

Comment on lines +74 to +90
## Open assumptions

1. **Pi schema is unverified against a real install.** Taken from published docs
only (`001` §2). The cycle that ships Pi must diff against a real
`~/.pi/agent/models.json` or ship the Pi client marked experimental.
2. **The API page layout rework may or may not have landed** when `040` runs.
`003` §6 gives both placements; the panel is layout-independent.
3. **32k output budget** is a schema stand-in inherited from `ocx opencode`, not a
per-model truth. If an authoritative max-output field ever reaches
`CatalogModel`, both serializers should switch to it.

## Acceptance

The unit is DONE when all four phases have closed with their evidence, `bun run
typecheck` and `bun run test` are green, `bun run privacy:scan` is green, and a
config exported from each of the four surfaces is byte-identical for the same
client and catalog state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- plan files ---'
cat -n devlog/_plan/260731_client_config_export/000_plan.md | sed -n '1,130p'
printf '%s\n' '--- core phase ---'
cat -n devlog/_plan/260731_client_config_export/010_export_core.md | sed -n '1,155p'
printf '%s\n' '--- related Pi references ---'
rg -n -i -C 3 'Pi|models\.json|experimental|byte-identical|acceptance|serializer' devlog/_plan/260731_client_config_export

Repository: lidge-jun/opencodex

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- phase map and acceptance references ---'
rg -n -C 4 'Phase|Accept criteria|Acceptance|DONE|BLOCKED|NEEDS_HUMAN|real .*install|experimental|diff against' \
  devlog/_plan/260731_client_config_export/{000_plan.md,010_export_core.md,020_cli_surface.md,030_management_api.md,040_gui_panel.md,050_docs_and_hardening.md}

printf '%s\n' '--- all repository references to Pi export status ---'
rg -n -i -C 2 'Pi.*(schema|experimental|install|models\.json)|models\.json.*Pi|experimental.*Pi|Pi.*stable|stable.*Pi' \
  --glob '!devlog/_plan/260731_client_config_export/001_client_config_survey.md' \
  .

Repository: lidge-jun/opencodex

Length of output: 50378


🌐 Web query:

Pi coding agent custom provider models.json schema providers models contextWindow maxTokens apiKey current documentation

💡 Result:

The Pi coding agent allows you to define custom providers and models using a models.json file located at ~/.pi/agent/models.json [1][2]. Models.json Schema Structure The schema is defined as an object containing a providers record, where each key represents a provider name [3][4]. providers (Record): A dictionary where keys are provider identifiers [3]. - baseUrl (string, optional): The API endpoint URL [1][5]. - api (string, optional): The API type (e.g., openai-completions, anthropic-messages) [1][6]. - apiKey (string, optional): API key for the provider [1][2]. It is often better to omit this in the file if using auth.json or CLI flags for security [1][7]. - headers (Record, optional): Custom headers [1][5]. - models (Array, optional): A list of model configurations [1][2]. - modelOverrides (Record, optional): Per-model configuration overrides [1][5]. Model Configuration Fields Each entry in the models array uses the following fields [1][5]: - id (string, required): The internal model identifier [1][2]. - name (string, optional): A human-readable label [1][2]. - contextWindow (number, optional): Maximum context window in tokens (default: 128000) [1][2]. - maxTokens (number, optional): Maximum output tokens (default: 16384) [1][2]. - reasoning (boolean, optional): Whether the model supports extended thinking [1][5]. - input (array, optional): Supported input types, e.g., ["text", "image"] [1][6]. - cost (object, optional): Token pricing for usage tracking (input, output, cacheRead, cacheWrite) [6][8]. - compat (object, optional): Provider compatibility overrides [1][6]. Credential Resolution Order When the agent resolves an API key or configuration for a provider, it follows this priority order [7][9]: 1. CLI --api-key flag [7][9] 2. auth.json entry [7][9] 3. Environment variables [7][9] 4. Custom provider keys defined directly in models.json [7][9] If no authentication is configured via the first three methods, models defined in models.json will load but remain unavailable for use in the CLI [1][2]. For complex requirements like custom OAuth or custom API implementations, the documentation recommends creating an extension instead of using models.json [6][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact plan acceptance and Pi gate ---'
sed -n '74,100p' devlog/_plan/260731_client_config_export/000_plan.md
sed -n '109,126p' devlog/_plan/260731_client_config_export/010_export_core.md
sed -n '1,115p' devlog/_plan/260731_client_config_export/050_docs_and_hardening.md

printf '%s\n' '--- phase-specific acceptance criteria ---'
for f in devlog/_plan/260731_client_config_export/020_cli_surface.md \
         devlog/_plan/260731_client_config_export/030_management_api.md \
         devlog/_plan/260731_client_config_export/040_gui_panel.md; do
  echo "### $f"
  rg -n -A 18 '^## Accept criteria|^## Acceptance|^## Status' "$f"
done

printf '%s\n' '--- implemented Pi serializer and tests ---'
sed -n '300,375p' src/clients/config-export.ts
sed -n '120,185p' tests/client-config-export.test.ts
sed -n '1,180p' tests/management-client-config-route.test.ts | tail -n 50

printf '%s\n' '--- repository state and Pi runtime dependencies ---'
git status --short
rg -n -i 'pi-mono|earendil-works/pi|badlogic/pi|models\.json' package.json bun.lockb bun.lock docs-site src tests gui 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 24371


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- Pi guide status ---'
cat -n docs-site/src/content/docs/guides/pi.md | sed -n '100,122p'

printf '%s\n' '--- exact source and test evidence ---'
cat -n src/clients/config-export.ts | sed -n '304,325p'
cat -n tests/client-config-export.test.ts | sed -n '205,285p'

printf '%s\n' '--- repository evidence of a real-install fixture or validator ---'
rg -n -i 'real (pi )?install|pi (schema|config).*(validate|accept|parse|load)|models\.json.*(fixture|golden|snapshot)|pi.*(fixture|golden|snapshot)' \
  devlog docs-site src tests gui package.json bun.lock bun.lockb 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 49191


Make Pi schema validation a release gate.

The caveat in 000_plan.md#L74-L78 is not part of the DONE criteria at #L85-L90. 010_export_core.md#L121-L124 defers the decision to a later cycle, while 050_docs_and_hardening.md#L1-L4 ships after the four implementation phases and only documents the unverified status. Project tests validate our JSON shape, not Pi's ability to load ~/.pi/agent/models.json; a schema mismatch can make every exported Pi config unusable.

Add real-install Pi validation to the unit DONE criteria and block Pi from stable CLI, API, and GUI surfaces until it passes. Otherwise, mark Pi export experimental in the plan, all surfaces, and documentation.

📍 Affects 2 files
  • devlog/_plan/260731_client_config_export/000_plan.md#L74-L90 (this comment)
  • devlog/_plan/260731_client_config_export/010_export_core.md#L121-L124
🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/000_plan.md` around lines 74 - 90,
The plan must make real-install Pi schema validation a release gate: update the
DONE criteria in 000_plan.md to require successful validation against an actual
~/.pi/agent/models.json and prevent Pi from stable CLI, API, and GUI surfaces
until it passes; update 010_export_core.md at its deferred decision to reflect
this requirement. If validation cannot be made a gate, consistently mark Pi
export experimental across the plan, all surfaces, and documentation.

Comment on lines +96 to +99
## Status

Roadmap written 2026-07-31 during the docs-only cycle. No implementation has
started; no production file has been modified by this unit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the roadmap status.

The status says that implementation has not started, but devlog/_plan/260731_client_config_export/010_export_core.md contains August 1, 2026 implementation amendments and the PR objectives describe completed implementation phases. Record the current phase and verification evidence instead of leaving this historical status active.

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/000_plan.md` around lines 96 - 99,
Update the Status section of the roadmap to reflect the current implementation
phase rather than the historical docs-only state. Summarize the completed
implementation phases and include verification evidence based on the amendments
and PR objectives documented in 010_export_core.md; remove the claim that no
implementation has started or no production files were modified.

Comment on lines +154 to +161
**Merge semantics are stated, not implied.** The downloaded file contains ONLY our
provider block plus `$schema`. A user with an existing `opencode.json` must merge,
not replace — replacing would destroy their other providers and MCP config. One
line of text prevents a support issue that would otherwise be data loss.

Accessibility: the download button is a real `<button>`, the result is announced
through the existing live region, and the anchor trick stays an implementation
detail — never a bare `<a download>` styled as a button.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Make the merge instructions client-specific.

The statement that every downloaded file contains a provider block plus $schema is true for OpenCode, but the planned Pi document has a top-level providers object and no $schema (devlog/_plan/260731_client_config_export/010_export_core.md, Lines 94-106).

Describe the OpenCode and Pi merge shapes separately while keeping the warning not to replace an existing configuration.

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/003_export_ux_design.md` around
lines 154 - 161, Update the merge-semantics section to describe each client’s
downloaded configuration shape separately: OpenCode includes its provider block
and $schema, while Pi uses a top-level providers object without $schema. Keep
the explicit warning that users must merge these blocks into existing
configuration rather than replace the file.

Comment on lines +29 to +58
```ts
export interface ExportModel {
/** Canonical proxy selector: `provider/id`, or bare slug for native. */
namespaced: string;
provider: string;
id: string;
displayName?: string;
contextWindow?: number;
inputModalities?: string[];
}

export interface ExportContext {
baseUrl: string; // http://host:port/v1
models: readonly ExportModel[];
}

export type ExportClientId = "opencode" | "pi";

export interface ExportClientSpec {
id: ExportClientId;
/** Download filename; matches the destination file's own name (003 §5). */
filename: string;
/** Canonical destination for humans. Not written to. */
destination: (env: NodeJS.ProcessEnv) => string;
/** Env var the config references; the value is never serialized. */
apiKeyEnv: string;
/** Shell line the user runs before launching the client. */
exportHint: string;
build: (ctx: ExportContext) => unknown;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(010_export_core\.md|164|176|003|.*export.*|.*opencode.*|.*launcher.*)$' | head -200

printf '%s\n' '--- referenced identifiers and terms ---'
rg -n --hidden -S 'OcxConfig|ExportContext|ExportModel|x-opencodex-api-key|options\.apiKey|apiKey|loopback|non-loopback|OpenCode' \
  --glob '!node_modules' --glob '!dist' --glob '!build' . | head -300

Repository: lidge-jun/opencodex

Length of output: 43062


🏁 Script executed:

#!/bin/bash
set -eu

plan='devlog/_plan/260731_client_config_export/010_export_core.md'
if [ -f "$plan" ]; then
  printf '%s\n' '--- plan section ---'
  cat -n "$plan" | sed -n '1,220p'
fi

printf '%s\n' '--- files in export plan directory ---'
find devlog/_plan/260731_client_config_export -maxdepth 1 -type f -print 2>/dev/null | sort | while read -r f; do
  printf '%s\n' "--- $f"
  wc -l "$f"
done

Repository: lidge-jun/opencodex

Length of output: 9441


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- all OcxConfig definitions and uses ---'
rg -n -C 5 --hidden -S 'OcxConfig' --glob '!node_modules' --glob '!dist' --glob '!build' . | head -500

printf '%s\n' '--- export contract definitions and call sites ---'
rg -n -C 4 --hidden -S 'ExportContext|ExportClientSpec|build\s*[:=]|build\(' \
  --glob '!node_modules' --glob '!dist' --glob '!build' . | head -500

Repository: lidge-jun/opencodex

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- relevant plan references ---'
rg -n -C 8 -S 'OcxConfig|x-opencodex-api-key|options\.apiKey|loopback|non-loopback|bind|164|176|ExportContext|ExportClientSpec' \
  devlog/_plan devlog 2>/dev/null | head -600

printf '%s\n' '--- repository file overview for likely source files ---'
git ls-files | sed -n '1,240p'

Repository: lidge-jun/opencodex

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- config-export outline ---'
ast-grep outline src/clients/config-export.ts --view compact

printf '%s\n' '--- config-export implementation ---'
cat -n src/clients/config-export.ts | sed -n '1,390p'

printf '%s\n' '--- OpenCode launcher implementation and exports ---'
rg -n -C 8 -S 'opencodeProviderOptions|shouldInjectApiAuthHeader|buildOpencodeProviderBlockFromCatalog|opencodeModelEntryLabel|OPENCODE_API_KEY_ENV|export' src/cli/opencode.ts src/clients/config-export.ts

Repository: lidge-jun/opencodex

Length of output: 338


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- OcxConfig server-binding and auth fields ---'
cat -n src/types.ts | sed -n '531,620p'
rg -n -C 10 -S 'function shouldInjectApiAuthHeader|shouldInjectApiAuthHeader|function opencodeProviderOptions|opencodeProviderOptions|buildOpencodeProviderBlockFromCatalog|opencodeGlobalConfigPath' src tests

printf '%s\n' '--- export callers and context construction ---'
rg -n -C 12 -S 'buildClientConfig|EXPORT_CLIENTS|ExportContext|config:' src tests gui devlog/_plan/260731_client_config_export

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- export tests ---'
cat -n tests/client-config-export.test.ts | sed -n '1,420p'

printf '%s\n' '--- OpenCode tests mentioning bind/auth output ---'
rg -n -C 12 -S 'x-opencodex-api-key|options|apiKey|hostname|loopback|non-loopback|buildOpencodeProviderBlockFromCatalog|opencodeProviderOptions' \
  tests/opencode-cli.test.ts tests/client-config-export.test.ts tests/ocx-launcher-runtime.test.ts tests/ocx-launcher-source.test.ts src/cli/opencode.ts src/clients/config-export.ts

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- plan phases that define caller contracts ---'
cat -n devlog/_plan/260731_client_config_export/020_cli_surface.md | sed -n '1,150p'
cat -n devlog/_plan/260731_client_config_export/030_management_api.md | sed -n '1,130p'
cat -n devlog/_plan/260731_client_config_export/040_gui_panel.md | sed -n '1,155p'

printf '%s\n' '--- exact source call sites (excluding plan text) ---'
rg -n -C 10 -S 'buildClientConfig|ExportContext|EXPORT_CLIENTS' src tests gui --glob '!**/*.map'

Repository: lidge-jun/opencodex

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

for path in [
    Path("src/clients/config-export.ts"),
    Path("src/cli/opencode.ts"),
    Path("tests/client-config-export.test.ts"),
]:
    print(f"--- {path} ---")
    text = path.read_text()
    needles = (
        "interface ExportContext",
        "opencodeProviderOptions",
        "shouldInjectApiAuthHeader",
        "buildOpencodeProviderBlockFromCatalog",
        "buildClientConfig",
        "x-opencodex-api-key",
        "hostname",
        "config:",
        "ExportContext",
    )
    lines = text.splitlines()
    hits = [i for i, line in enumerate(lines) if any(n in line for n in needles)]
    ranges = []
    for i in hits:
        start, end = max(0, i - 8), min(len(lines), i + 16)
        if ranges and start <= ranges[-1][1]:
            ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
        else:
            ranges.append((start, end))
    for start, end in ranges:
        for i in range(start, end):
            print(f"{i+1:4}\t{lines[i]}")
        print()
PY

Repository: lidge-jun/opencodex

Length of output: 32398


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- source status and diff summary ---'
git status --short
git diff --stat -- src/clients/config-export.ts src/cli/opencode.ts tests/client-config-export.test.ts

printf '%s\n' '--- exact current symbols ---'
rg -n -S 'export (function|const|interface|type)|function (opencode|shouldInject)|x-opencodex-api-key|options\.apiKey|headers' \
  src/clients/config-export.ts src/cli/opencode.ts tests/client-config-export.test.ts

Repository: lidge-jun/opencodex

Length of output: 5795


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CLI export context construction ---'
rg -n -C 18 -S 'loadConfig|buildClientConfig|baseUrl|models|config' src/cli/export-command.ts

printf '%s\n' '--- management route context construction ---'
cat -n src/server/management/model-routes.ts | sed -n '235,286p'

printf '%s\n' '--- plan snippets that may remain stale ---'
cat -n devlog/_plan/260731_client_config_export/010_export_core.md | sed -n '23,90p'
cat -n devlog/_plan/260731_client_config_export/010_export_core.md | sed -n '158,178p'
cat -n devlog/_plan/260731_client_config_export/020_cli_surface.md | sed -n '64,78p'

Repository: lidge-jun/opencodex

Length of output: 15438


Synchronize the export-plan snippets with the implemented contract.

src/cli/export-command.ts and src/server/management/model-routes.ts already pass config, and tests cover both bind modes. Update 010_export_core.md:29-43 with native?: boolean and config?: OcxConfig. Update :74-88 with the conditional options.headers["x-opencodex-api-key"] output. Update 020_cli_surface.md:64-72 to include config.

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/010_export_core.md` around lines 29
- 58, The export-plan documentation is missing fields and header behavior
already required by the implemented contract. Update ExportModel with native?:
boolean and ExportContext with config?: OcxConfig, document conditional output
of options.headers["x-opencodex-api-key"], and update the CLI surface snippet to
include config; keep the existing bind-mode behavior and client interfaces
unchanged.

Comment on lines +63 to +66
Shared precondition applied before either serializer runs: drop disabled rows,
drop duplicate `namespaced` (first wins, native leads), sort by `namespaced` so
output is byte-stable across calls. Stability matters because the GUI shows a
diffable preview and agents may checksum the payload.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make duplicate resolution deterministic before sorting.

first wins makes output depend on input order when duplicate namespaced rows have different labels, context limits, or modalities. Sorting after deduplication does not restore determinism, so shuffled catalog rows can produce different configuration bytes.

Sort by a deterministic precedence key before deduplication, or define an explicit duplicate comparator. Add a fixture with conflicting duplicate rows.

Also applies to: 150-155

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/010_export_core.md` around lines 63
- 66, Make duplicate resolution deterministic in the shared precondition before
either serializer runs: establish a stable precedence order for rows sharing the
same namespaced value, then retain the highest-precedence row before sorting the
deduplicated results. Update the affected export flows consistently and add a
fixture containing conflicting duplicate rows to verify identical output
regardless of input order.

Comment on lines +109 to +119
Decisions, each traceable to `002` §4:

| Field | Emission rule | Why |
|-------|---------------|-----|
| `id` | `namespaced` | matches what the proxy actually routes |
| `name` | `displayName ?? namespaced` | same label rule as OpenCode |
| `contextWindow` | omit when unknown | never guess |
| `maxTokens` | `min(32000, contextWindow)`, omit when context unknown | same stand-in as OpenCode, same clamp |
| `cost` | **omit always** | we have no price data; zeros would assert "free", which is false for routed providers |
| `input` | from `inputModalities`, default `["text"]` | text is the one modality every routed model supports |
| `reasoning` | omit | `reasoningEfforts` is an effort list, not Pi's boolean; mapping would be a guess |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use one model-label rule throughout the plan.

The field table says displayName ?? namespaced, but the amendment says existing behavior appends (<native|provider|routed>) and that Pi reuses the same label. Update the earlier field table, examples, and golden-fixture expectations to the richer rule.

Otherwise an implementation can follow the stale section and break the required pre-refactor OpenCode parity.

Also applies to: 164-167

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/010_export_core.md` around lines 109
- 119, Use the richer existing model-label rule consistently throughout the
plan: update the field table, examples, and golden-fixture expectations so name
values append the appropriate (native|provider|routed) suffix instead of using
only displayName ?? namespaced. Ensure the Pi export reuses this same label
behavior and preserves pre-refactor OpenCode parity.

Comment on lines +94 to +102
api.clientConfig.title api.clientConfig.copy
api.clientConfig.clientOpencode api.clientConfig.download
api.clientConfig.clientPi api.clientConfig.destination
api.clientConfig.mergeWarning api.clientConfig.envHint
api.clientConfig.modelCount api.clientConfig.missingLimits
api.clientConfig.noKeyYet api.clientConfig.loadFailed
api.clientConfig.retry api.clientConfig.downloadedAnnounce
api.clientConfig.copiedAnnounce api.clientConfig.whereDisclosure
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the i18n key count.

The list contains sixteen keys, but the file-change map requires fifteen keys per locale. Reconcile the count and add a test that verifies every listed api.clientConfig.* key exists in all six locale files.

Also applies to: 122-123

🤖 Prompt for 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.

In `@devlog/_plan/260731_client_config_export/040_gui_panel.md` around lines 94 -
102, Reconcile the api.clientConfig key list in the locale file-change map to
the required fifteen keys, removing or correcting the extra entry while
preserving the intended translations. Add a test that iterates over every listed
api.clientConfig.* key and verifies it exists in all six locale files.

Comment on lines +465 to +469
:::caution[Merge, never replace]
`ocx export` never writes your real client config. The destination is printed for you to merge by
hand, and `--out` refuses to overwrite an existing file without `--force`, because replacing a
config destroys the other providers, agents, and MCP entries already in it.
:::

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 12 'canonical|destination|--out|force|writeFile|opencode\.json|models\.json' \
  src/cli/export-command.ts src/cli/help.ts tests/cli-export-command.test.ts

Repository: lidge-jun/opencodex

Length of output: 26787


Clarify --out behavior in all export documentation

Without --out, ocx export prints the configuration and does not modify the canonical client file. With --out <path>, it writes to the requested path. --force replaces an existing path, including a canonical client configuration path; the CLI does not reject it.

Update docs-site/src/content/docs/reference/cli.md:465-469, both guide pages, and the ja, ko, ru, and zh-cn translations to state this distinction.

📍 Affects 7 files
  • docs-site/src/content/docs/reference/cli.md#L465-L469 (this comment)
  • docs-site/src/content/docs/guides/opencode.md#L61-L64
  • docs-site/src/content/docs/guides/pi.md#L57-L60
  • docs-site/src/content/docs/ja/reference/cli.md#L358-L361
  • docs-site/src/content/docs/ko/reference/cli.md#L405-L408
  • docs-site/src/content/docs/ru/reference/cli.md#L399-L403
  • docs-site/src/content/docs/zh-cn/reference/cli.md#L349-L352
🤖 Prompt for 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.

In `@docs-site/src/content/docs/reference/cli.md` around lines 465 - 469, Clarify
the `ocx export` documentation at
docs-site/src/content/docs/reference/cli.md:465-469,
docs-site/src/content/docs/guides/opencode.md:61-64,
docs-site/src/content/docs/guides/pi.md:57-60,
docs-site/src/content/docs/ja/reference/cli.md:358-361,
docs-site/src/content/docs/ko/reference/cli.md:405-408,
docs-site/src/content/docs/ru/reference/cli.md:399-403, and
docs-site/src/content/docs/zh-cn/reference/cli.md:349-352: without `--out`,
output is printed and no canonical client file is modified; with `--out <path>`,
the requested path is written; and `--force` permits replacing any existing
path, including a canonical client configuration, rather than being rejected.
Update each translation consistently.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant