-
Notifications
You must be signed in to change notification settings - Fork 517
feat(export): client config export for Pi and OpenCode #852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c08fff
7033153
51cfd77
ef21ae7
e53f2d9
9e717c9
59e8009
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # 000 — Client config export for Pi and OpenCode | ||
|
|
||
| Unit: `devlog/_plan/260731_client_config_export/` | ||
| Opened: 2026-07-31 · Work class: C3 · Branch target: `dev` | ||
|
|
||
| ## Objective | ||
|
|
||
| Let a user or an agent take 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 serve the same bytes: CLI text, CLI `--json`, management API, and a GUI | ||
| panel with copy + JSON download. | ||
|
|
||
| ## Why this unit exists | ||
|
|
||
| The proxy already speaks the wire every client understands. What it does 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. Today a user | ||
| wanting opencodex in Pi writes that JSON by hand, and gets it wrong, because model | ||
| ids are namespaced and context windows are not guessable. | ||
|
|
||
| `ocx opencode` solves half of this already, in memory, at launch, for one client | ||
| (`002`). This unit turns that private capability into an artifact and adds Pi. | ||
|
|
||
| ## Constraints | ||
|
|
||
| | Constraint | Source | | ||
| |-----------|--------| | ||
| | Never serialize a live key; emit an env reference | AGENTS.md security boundary; `001` §3 | | ||
| | Never write or rewrite a user's client config file | existing `ocx opencode` design; `002` §2 | | ||
| | Never guess model metadata; omit what we do not know | existing `limit`-drop rule; `002` §4 | | ||
| | One payload, four presentations | `003` §2 | | ||
| | OpenCode V1 schema only | `001` §2 | | ||
| | Bun-native TypeScript, `dev` branch | AGENTS.md | | ||
|
|
||
| ## Scope boundary | ||
|
|
||
| IN | ||
| - Client-neutral export core with OpenCode + Pi serializers. | ||
| - `ocx export --client <id> [--json] [--out]`. | ||
| - `GET /api/client-config?client=<id>`. | ||
| - GUI panel: client switch, JSON preview, copy, download. | ||
| - Tests at each phase; docs-site update when the CLI surface lands. | ||
|
|
||
| OUT | ||
| - OpenCode V2 schema migration (recorded in `001`, not built). | ||
| - Automatic merging into an existing user config file. | ||
| - Any client beyond Pi and OpenCode. Claude Desktop already has its own path. | ||
| - Changes to routing, adapters, or the catalog itself. | ||
| - Pricing metadata. We do not have it; `cost` is omitted rather than faked. | ||
|
|
||
| ## Work-phase map | ||
|
|
||
| Dependency-ordered. Each phase is one full PABCD cycle and closes with something | ||
| independently verifiable. | ||
|
|
||
| | Phase | Doc | Delivers | Verified by | | ||
| |-------|-----|----------|-------------| | ||
| | 1 | `010_export_core.md` | `src/clients/config-export.ts`; OpenCode builder relocated, Pi serializer added | unit tests + golden diff against pre-refactor output | | ||
| | 2 | `020_cli_surface.md` | `ocx export` with `--json` / `--out` | CLI tests; stdout parses clean under `--json` | | ||
| | 3 | `030_management_api.md` | `GET /api/client-config` | route test; payload identical to CLI `--json` | | ||
| | 4 | `040_gui_panel.md` | GUI panel with copy + download | GUI test + rendered check | | ||
|
|
||
| The order is the build order, not a schedule: `020`, `030`, `040` each import the | ||
| core, and `040` renders what `030` returns. Nothing later can be built first. | ||
|
|
||
| ## Research documents | ||
|
|
||
| | Doc | Contents | | ||
| |-----|----------| | ||
| | `001_client_config_survey.md` | Pi + OpenCode V1/V2 schemas, injection points, absence of a standard, V1 decision | | ||
| | `002_existing_surface_inventory.md` | What `ocx opencode` already does, the corrected gap, catalog metadata availability | | ||
| | `003_export_ux_design.md` | Design Read, dials, Lazy-User Gate, five UX states, download rules, placement | | ||
|
|
||
| ## 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. | ||
|
Comment on lines
+74
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_exportRepository: 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:
💡 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 || trueRepository: 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 || trueRepository: lidge-jun/opencodex Length of output: 49191 Make Pi schema validation a release gate. The caveat in Add real-install Pi validation to the unit 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| Terminal outcomes other than DONE: `BLOCKED` if Pi's real schema contradicts the | ||
| survey badly enough to need a new design; `NEEDS_HUMAN` if a decision outside this | ||
| plan's boundary appears. | ||
|
|
||
| ## Status | ||
|
|
||
| Roadmap written 2026-07-31 during the docs-only cycle. No implementation has | ||
| started; no production file has been modified by this unit. | ||
|
Comment on lines
+96
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # 001 — External client config survey | ||
|
|
||
| Research only. No diffs here; implementation designs live in the decade docs. | ||
|
|
||
| Scope of the survey: where a user (or an agent) drops a custom OpenAI-compatible | ||
| provider so that Pi and OpenCode can call the opencodex proxy, what JSON those | ||
| files require, and whether any cross-vendor standard governs the shape. | ||
|
|
||
| ## 1. Is there a standard? | ||
|
|
||
| No. There is no formal specification body governing agent-client provider | ||
| configuration. What exists instead is a stack of de-facto layers: | ||
|
|
||
| | Layer | What it standardizes | Who honors it | | ||
| |-------|----------------------|---------------| | ||
| | OpenAI `/v1` wire (`/chat/completions`, `/responses`, `/models`) | request/response bytes | effectively everyone | | ||
| | Anthropic `/v1/messages` wire | request/response bytes | Claude clients, our Messages surface | | ||
| | `@ai-sdk/openai-compatible` (Vercel AI SDK) | adapter package name + option names | OpenCode, several TS agents | | ||
| | models.dev | model metadata registry (context, cost, modality) | OpenCode, Pi metadata seeds | | ||
| | per-client JSON Schema (`https://opencode.ai/config.json`) | one client's config file only | that client | | ||
|
|
||
| The consequence for this unit: the transport half is already solved — the proxy | ||
| speaks `/v1` and clients speak `/v1`. Everything left is **metadata translation** | ||
| into per-client dialects, and each dialect is a moving target owned by one vendor. | ||
|
|
||
| Critically, **models.dev does not populate custom providers.** A user-defined | ||
| provider block gets no inherited context window, no pricing, no modality flags. | ||
| Whatever we do not emit ourselves, the client simply does not know. | ||
|
|
||
| ## 2. Where custom providers are injected | ||
|
|
||
| ### OpenCode | ||
|
|
||
| Three layers, highest priority first: | ||
|
|
||
| | Layer | Location | Notes | | ||
| |-------|----------|-------| | ||
| | Inline runtime | `OPENCODE_CONFIG_CONTENT` env var | outranks every file; what `ocx opencode` uses today | | ||
| | Project | `<git root>/opencode.json` or `.jsonc` | committed per repo | | ||
| | Global | `~/.config/opencode/opencode.json`, XDG-aware via `XDG_CONFIG_HOME` | the file most users hand-edit | | ||
|
|
||
| JSONC is accepted for the file layers (comments must survive a round trip, which | ||
| is why we never rewrite a user's file — see `002`). | ||
|
|
||
| V1 schema (current, what we generate): | ||
|
|
||
| ```json | ||
| { | ||
| "$schema": "https://opencode.ai/config.json", | ||
| "provider": { | ||
| "opencodex": { | ||
| "npm": "@ai-sdk/openai-compatible", | ||
| "name": "opencodex", | ||
| "options": { "baseURL": "http://127.0.0.1:10100/v1", "apiKey": "{env:OPENCODEX_OPENCODE_API_KEY}" }, | ||
| "models": { | ||
| "anthropic/claude-opus-5": { "name": "Claude Opus 5", "limit": { "context": 200000, "output": 32000 } } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| V2 schema (documented at `opencode.ai/v2/docs/models`) renames nearly every key: | ||
|
|
||
| | Concept | V1 | V2 | | ||
| |---------|----|----| | ||
| | container | `provider` | `providers` | | ||
| | adapter | `npm: "@ai-sdk/openai-compatible"` | `package: "aisdk:@ai-sdk/openai-compatible"` | | ||
| | endpoint | `options.baseURL` | `settings.baseURL` | | ||
| | upstream id | `models.<k>.id` | `models.<k>.modelID` | | ||
| | credentials | `options.apiKey` / auth store | `env[]` | | ||
| | new in V2 | — | `capabilities`, `variants`, `disabled` | | ||
|
|
||
| **Decision: stay on V1.** The shipped launcher already emits V1 and that path is | ||
| exercised by tests; switching would break it in exchange for `capabilities` and | ||
| `variants` fields we have no authoritative data to fill. V2 is recorded here so a | ||
| later migration starts from a written diff rather than a re-survey. | ||
|
|
||
| ### Pi | ||
|
|
||
| Single global file: `~/.pi/agent/models.json`. | ||
|
|
||
| ```json | ||
| { | ||
| "providers": { | ||
| "opencodex": { | ||
| "baseUrl": "http://127.0.0.1:10100/v1", | ||
| "api": "openai-completions", | ||
| "apiKey": "$OPENCODEX_API_KEY", | ||
| "models": [ | ||
| { | ||
| "id": "anthropic/claude-opus-5", | ||
| "name": "Claude Opus 5", | ||
| "reasoning": false, | ||
| "input": ["text"], | ||
| "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, | ||
| "contextWindow": 200000, | ||
| "maxTokens": 32000 | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
Comment on lines
+83
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Do not present zero cost as exported metadata. The Pi example includes 🤖 Prompt for AI Agents |
||
|
|
||
| Shape differences that matter for the serializer: | ||
|
|
||
| - `models` is an **array**, not a keyed object. Model identity lives in `id`. | ||
| - `cost` demands all four fields when present. | ||
| - `apiKey` supports `$ENV_VAR` interpolation (bare `$NAME`, not `{env:NAME}`). | ||
| - `api` selects the wire dialect; `openai-completions` is ours. | ||
|
|
||
| **Verification status: UNVERIFIED against a real installation.** The shape above | ||
| comes from Pi's published `models.md` and `pi.dev/docs/latest/custom-provider`. | ||
| No `~/.pi/agent/models.json` was read on this machine. The first implementation | ||
| cycle touching Pi must diff this against a real file before shipping. | ||
|
|
||
| ### Cross-client field map | ||
|
|
||
| | Concept | Pi | OpenCode V1 | | ||
| |---------|----|-------------| | ||
| | container | `providers` | `provider` | | ||
| | adapter | `api: "openai-completions"` | `npm: "@ai-sdk/openai-compatible"` | | ||
| | endpoint | `baseUrl` | `options.baseURL` | | ||
| | model container | array | keyed object | | ||
| | context | `contextWindow` | `limit.context` | | ||
| | max output | `maxTokens` | `limit.output` | | ||
| | price | `cost{4}` | absent | | ||
| | modality | `input[]` | absent | | ||
| | key reference | `$VAR` | `{env:VAR}` | | ||
|
|
||
| Only four concepts are universal: endpoint, adapter kind, model id, and the | ||
| context/output pair. Everything else is one client's luxury. | ||
|
|
||
| ## 3. Credential handling | ||
|
|
||
| All three injection paths support an environment reference, so no exporter needs | ||
| to serialize a live `ocx_...` key. Pi takes `$VAR`, OpenCode takes `{env:VAR}`, | ||
| and OpenCode additionally has an auth store. AGENTS.md treats token serialization | ||
| as a release blocker, so emitting a reference is not merely polite — writing the | ||
| plaintext key into an exported file would be a security-review failure. | ||
|
|
||
| The exported artifact therefore carries a variable name, and every surface that | ||
| hands the file over must also tell the user which variable to export. | ||
|
|
||
| ## 4. Sources | ||
|
|
||
| - Pi: `github.com/earendil-works/pi` `packages/coding-agent/docs/models.md`; `pi.dev/docs/latest/custom-provider` | ||
| - OpenCode V1: `opencode.ai/docs/providers`, `opencode.ai/config.json` | ||
| - OpenCode V2: `opencode.ai/v2/docs/models` | ||
| - AI SDK adapter: `@ai-sdk/openai-compatible` | ||
| - Metadata registry: `models.dev` | ||
|
|
||
| All external claims were read during the 2026-07-31 P phase. Client schemas drift; | ||
| re-verify before any cycle that changes serializer output. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # 002 — What already exists, and what the real gap is | ||
|
|
||
| Research only. Written because the first pass of this unit's P phase proposed | ||
| building things that were already shipped. Recording the correction here so a | ||
| later cycle's P does not repeat the same miss. | ||
|
|
||
| ## 1. Correction to the initial survey | ||
|
|
||
| The opening research turn concluded that OpenCode export had to be built from | ||
| scratch and listed three "design blockers" (slash-bearing model ids, credential | ||
| leakage, missing `limit.output`). All three were already solved in | ||
| `src/cli/opencode.ts`, a 701-line module that predates this unit. | ||
|
|
||
| | Claimed blocker | Actual state | | ||
| |-----------------|--------------| | ||
| | `provider/model` ids produce a double slash | `opencodeModelKey()` emits `provider/id` for routed and a bare slug for native; OpenCode splits on the FIRST slash, so the nested id survives | | ||
| | Exported config would leak `ocx_...` | Only `{env:OPENCODEX_OPENCODE_API_KEY}` is serialized; the real value rides the child process env | | ||
| | `limit.output` has no authoritative source | `SCHEMA_REQUIRED_OUTPUT_BUDGET` (32k, clamped to context) fills the schema-required half, and the whole `limit` block is dropped when no authoritative context exists | | ||
|
|
||
| The lesson is procedural: a survey of external formats is not a survey of our own | ||
| tree. Both halves are required before a phase map is credible. | ||
|
|
||
| ## 2. Inventory of the shipped surface | ||
|
|
||
| ### `src/cli/opencode.ts` | ||
|
|
||
| | Export | Role | | ||
| |--------|------| | ||
| | `buildOpencodeProviderBlockFromCatalog` | catalog rows -> V1 provider block. The real serializer core | | ||
| | `buildOpencodeProviderBlock` | test-facing wrapper over the same builder | | ||
| | `opencodeModelKey` | `provider/id` vs bare native slug | | ||
| | `opencodeProxyBaseUrl` | `http://<host>:<port>/v1` | | ||
| | `opencodeCatalogFromProxyRows` | `GET /api/models` rows -> catalog entries, drops disabled + dupes | | ||
| | `mergeOpencodeRuntimeConfig` | merges inherited inline config, overrides only `provider.opencodex` | | ||
| | `buildOpencodeConfig` / `serializeOpencodeRuntimeConfig` | assemble + stringify the inline runtime layer | | ||
| | `opencodeGlobalConfigPath` / `opencodeProviderOverridePath` / `projectConfigOverridesProvider` | detect (never write) user files | | ||
|
|
||
| Design properties worth preserving verbatim: | ||
|
|
||
| - **Never writes user config.** File layers are read for detection only; injection | ||
| goes through `OPENCODE_CONFIG_CONTENT`. This avoids clobbering comments, | ||
| relative `{file:...}` paths, and unrelated MCP credentials. | ||
| - **Never guesses metadata.** Absent context window means no `limit` block, not a | ||
| fabricated one. | ||
| - **Never serializes a secret.** | ||
|
|
||
| ### Adjacent precedents | ||
|
|
||
| - `src/cli/claude-desktop.ts` — writes a Claude Desktop 3P config. | ||
| - `gui/src/pages/ClaudeDesktop.tsx:313` — `exportProfile()` does Blob + | ||
| `URL.createObjectURL` + `anchor.download`, then announces to a live region. | ||
| This is the house pattern for browser-side JSON download and the 040 phase | ||
| reuses it rather than inventing a second one. | ||
| - `src/cli/access.ts` — the `ocx access ...` family establishes the CLI contract | ||
| this unit extends: `--json` on every subcommand, `printData(value, wantsJson, lines)` | ||
| emitting either raw JSON or human lines from the SAME value. | ||
| - `src/server/management/model-routes.ts:115` — `GET /api/models`, the authenticated | ||
| route whose rows already feed the OpenCode builder. | ||
|
|
||
| ## 3. The actual gap | ||
|
|
||
| `ocx opencode` produces a provider block **only in memory, only at launch**, and | ||
| only for OpenCode. Three things are missing: | ||
|
|
||
| 1. **No artifact.** A user who wants the provider block in their own | ||
| `~/.config/opencode/opencode.json` cannot get it out of opencodex. There is no | ||
| command that prints it and no file to download. | ||
| 2. **No Pi support at all.** No serializer, no command, no surface. | ||
| 3. **No shared abstraction.** The builder is private to the launcher module and | ||
| shaped around one client's schema, so a second client cannot reuse it as-is. | ||
|
|
||
| Everything this unit builds is downstream of closing those three. The transport, | ||
| the catalog, and the security posture are already correct. | ||
|
|
||
| ## 4. Catalog metadata availability | ||
|
|
||
| Checked because every serializer depends on it: | ||
|
|
||
| | Field | Source | Available? | | ||
| |-------|--------|-----------| | ||
| | context window | `CatalogModel.contextWindow`; `NATIVE_OPENAI_CONTEXT_OVERRIDES` + upstream `context_window` for native | yes, with a documented 128k fallback in `parsing.ts:287` | | ||
| | display name | `CatalogModel.displayName` | yes, optional | | ||
| | max output | none | **no** — 32k schema stand-in only | | ||
| | price | not on `CatalogModel` | **no** | | ||
| | input modality | `CatalogModel.inputModalities` | yes | | ||
| | reasoning | `CatalogModel.reasoningEfforts` | yes, as effort list | | ||
|
|
||
| Consequence for Pi: `cost` cannot be filled honestly. Emitting zeros would assert | ||
| "this model is free," which is false for routed providers. The 010 phase must | ||
| decide omit-vs-zero, and the survey's field table says `cost` is optional when the | ||
| key is absent — so omission is the honest option. | ||
|
Comment on lines
+79
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(002_existing_surface_inventory\.md|010_export_core\.md|.*parsing.*|.*catalog.*|.*export.*|.*route.*|.*gui.*)'
printf '%s\n' '--- references to contextWindow, missing limits, and 128k ---'
rg -n -C 3 'contextWindow|context_window|128k|missing limit|missing-limit|limit' \
devlog/_plan/260731_client_config_export \
--glob '*.md' --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.json' || trueRepository: lidge-jun/opencodex Length of output: 41181 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- inventory and export-contract sections ---'
sed -n '35,100p' devlog/_plan/260731_client_config_export/002_existing_surface_inventory.md
sed -n '20,125p' devlog/_plan/260731_client_config_export/010_export_core.md
printf '%s\n' '--- parser fallback ---'
sed -n '240,315p' src/codex/catalog/parsing.ts
printf '%s\n' '--- catalog model declarations and construction sites ---'
rg -n -C 5 'interface CatalogModel|type CatalogModel|contextWindow\s*[:=]|context_window|NATIVE_OPENAI_CONTEXT_OVERRIDES|SCHEMA_REQUIRED_OUTPUT_BUDGET' \
src/codex src/cli src/clients tests \
--glob '*.ts' --glob '*.tsx'Repository: lidge-jun/opencodex Length of output: 50378 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- CatalogModel shape ---'
sed -n '80,145p' src/codex/catalog/parsing.ts
printf '%s\n' '--- all ensureStrictCatalogFields call sites ---'
rg -n -C 4 'ensureStrictCatalogFields\(' src tests --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- catalog-to-export boundary ---'
sed -n '1,125p' src/cli/export-command.ts
sed -n '150,225p' src/clients/config-export.ts
sed -n '280,345p' src/clients/config-export.ts
printf '%s\n' '--- relevant catalog fetch/assembly sections ---'
sed -n '730,815p' src/codex/catalog/provider-fetch.ts
sed -n '850,915p' src/codex/catalog/provider-fetch.tsRepository: lidge-jun/opencodex Length of output: 27095 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- fetchAllModels and management model route ---'
rg -n -C 6 'fetchAllModels|/api/models|contextWindow' \
src/server src/cli/opencode.ts src/cli/export-command.ts \
--glob '*.ts' --glob '*.tsx' | head -n 240
printf '%s\n' '--- parser output and sync entry points ---'
rg -n -C 5 'parse.*Catalog|read.*catalog|buildCatalogEntries|sync.*catalog|CatalogModel\[\]|models:\s*CatalogModel' \
src/codex/catalog src/server src/cli \
--glob '*.ts' | head -n 300
printf '%s\n' '--- API model row type and route body ---'
sed -n '90,155p' src/server/management/model-routes.ts
sed -n '1,120p' src/cli/opencode.tsRepository: lidge-jun/opencodex Length of output: 44943 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- management model-row completion ---'
sed -n '105,205p' src/server/management/model-routes.ts
printf '%s\n' '--- fetchAllModels implementation ---'
rg -n -C 12 'export async function fetchAllModels|function fetchAllModels' src/server/management/shared.ts src/server/management-api.ts
printf '%s\n' '--- gatherRoutedModels return path ---'
sed -n '408,575p' src/codex/catalog/provider-fetch.ts
sed -n '670,735p' src/codex/catalog/provider-fetch.ts
printf '%s\n' '--- raw catalog conversion path ---'
sed -n '295,365p' src/codex/catalog/sync.tsRepository: lidge-jun/opencodex Length of output: 21906 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
files = {
"parser": Path("src/codex/catalog/parsing.ts"),
"shared": Path("src/server/management/shared.ts"),
"route": Path("src/server/management/model-routes.ts"),
"export": Path("src/cli/export-command.ts"),
"contract": Path("src/clients/config-export.ts"),
"inventory": Path("devlog/_plan/260731_client_config_export/002_existing_surface_inventory.md"),
"core": Path("devlog/_plan/260731_client_config_export/010_export_core.md"),
}
text = {name: path.read_text() for name, path in files.items()}
def line_of(name, needle):
for i, line in enumerate(text[name].splitlines(), 1):
if needle in line:
return i
return None
checks = {
"parser fallback writes RawEntry.context_window=128000":
"entry.context_window = contextWindow" in text["parser"]
and " : 128000" in text["parser"],
"management fetchAllModels delegates to gatherRoutedModels":
"return gatherRoutedModels(config);" in text["shared"],
"management route copies contextWindow only when defined":
"...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {})" in text["route"],
"export boundary preserves contextWindow only when defined":
"...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {})" in text["export"],
"serializer omits context-derived fields when input is unknown":
"return undefined;" in text["contract"]
and "if (context !== undefined)" in text["contract"],
"export contract explicitly says unknown context is omitted":
"| `contextWindow` | omit when unknown" in text["core"],
"inventory still labels parser fallback as CatalogModel availability":
"`CatalogModel.contextWindow`" in text["inventory"]
and "128k fallback" in text["inventory"],
}
for label, result in checks.items():
print(f"{'PASS' if result else 'FAIL'}: {label}")
for name, needles in {
"parser": ["const contextWindow =", "entry.context_window = contextWindow"],
"shared": ["export async function fetchAllModels", "return gatherRoutedModels(config);"],
"route": ["async function listManagementModelRows", "row.contextWindow !== undefined"],
"export": ["exportModelsFromProxyRows", "entry.contextWindow !== undefined"],
"core": ["`contextWindow` | omit when unknown", "`maxTokens` |"],
}.items():
for needle in needles:
print(f"{name}:{line_of(name, needle)}: {needle}")
PYRepository: lidge-jun/opencodex Length of output: 1038 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
files = {
"parser": Path("src/codex/catalog/parsing.ts"),
"shared": Path("src/server/management/shared.ts"),
"route": Path("src/server/management/model-routes.ts"),
"export": Path("src/cli/export-command.ts"),
"contract": Path("src/clients/config-export.ts"),
"inventory": Path("devlog/_plan/260731_client_config_export/002_existing_surface_inventory.md"),
"core": Path("devlog/_plan/260731_client_config_export/010_export_core.md"),
}
text = {name: path.read_text() for name, path in files.items()}
def line_of(name, needle):
for i, line in enumerate(text[name].splitlines(), 1):
if needle in line:
return i
return None
checks = {
"parser fallback writes RawEntry.context_window=128000":
"entry.context_window = contextWindow" in text["parser"]
and " : 128000" in text["parser"],
"management fetchAllModels delegates to gatherRoutedModels":
"return gatherRoutedModels(config);" in text["shared"],
"management route copies contextWindow only when defined":
"...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {})" in text["route"],
"export boundary preserves contextWindow only when defined":
"...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {})" in text["export"],
"serializer omits context-derived fields when input is unknown":
"return undefined;" in text["contract"]
and "if (context !== undefined)" in text["contract"],
"export contract explicitly says unknown context is omitted":
"| `contextWindow` | omit when unknown" in text["core"],
"inventory still labels parser fallback as CatalogModel availability":
"`CatalogModel.contextWindow`" in text["inventory"]
and "128k fallback" in text["inventory"],
}
for label, result in checks.items():
print(f"{'PASS' if result else 'FAIL'}: {label}")
for name, needles in {
"parser": ["const contextWindow =", "entry.context_window = contextWindow"],
"shared": ["export async function fetchAllModels", "return gatherRoutedModels(config);"],
"route": ["async function listManagementModelRows", "row.contextWindow !== undefined"],
"export": ["exportModelsFromProxyRows", "entry.contextWindow !== undefined"],
"core": ["`contextWindow` | omit when unknown", "`maxTokens` |"],
}.items():
for needle in needles:
print(f"{name}:{line_of(name, needle)}: {needle}")
PYRepository: lidge-jun/opencodex Length of output: 1038 Document the parser-fallback boundary.
🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Reconcile the DONE gate with the documented verification plan.
The PR objective states that the full suite has pre-existing failures, but this criterion requires
bun run testto be green before DONE. Define the feature-specific test commands as the release gate and record unrelated baseline failures separately, or keep the unitBLOCKED.🤖 Prompt for AI Agents